From 8a1c8ad4bc9d152db1e4b8d2f304bc5077392a2d Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Sun, 16 Sep 2018 14:39:52 -0500 Subject: [PATCH 01/87] TST: Mock clipboard IO (#22715) * Attempt to fix clipboard tests * note * update * update * doc --- pandas/tests/io/test_clipboard.py | 62 +++++++++++++++++++++++++++---- setup.cfg | 1 + 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index a6b331685e72a..bb73c6bc6b38b 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -13,7 +13,6 @@ from pandas.util import testing as tm from pandas.util.testing import makeCustomDataframe as mkdf from pandas.io.clipboard.exceptions import PyperclipException -from pandas.io.clipboard import clipboard_set, clipboard_get try: @@ -76,10 +75,53 @@ def df(request): raise ValueError +@pytest.fixture +def mock_clipboard(mock, 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] + + mock_set = mock.patch("pandas.io.clipboard.clipboard_set", + side_effect=_mock_set) + mock_get = mock.patch("pandas.io.clipboard.clipboard_get", + side_effect=_mock_get) + with mock_get, mock_set: + 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") +@pytest.mark.usefixtures("mock_clipboard") class TestClipboard(object): + def check_round_trip_frame(self, data, excel=None, sep=None, encoding=None): data.to_clipboard(excel=excel, sep=sep, encoding=encoding) @@ -118,15 +160,18 @@ def test_copy_delim_warning(self, df): # 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): + 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 - assert clipboard_get().encode('utf-8') == df.to_csv(sep='\t') + result = mock_clipboard[request.node.name].encode('utf-8') + expected = df.to_csv(sep='\t') + assert result == expected else: - assert clipboard_get() == df.to_csv(sep='\t') + assert mock_clipboard[request.node.name] == df.to_csv(sep='\t') # Tests reading of white space separated tables @pytest.mark.parametrize('sep', [None, 'default']) @@ -138,7 +183,8 @@ def test_clipboard_copy_strings(self, sep, excel, df): assert result.to_string() == df.to_string() assert df.shape == result.shape - def test_read_clipboard_infer_excel(self): + def test_read_clipboard_infer_excel(self, request, + mock_clipboard): # gh-19010: avoid warnings clip_kwargs = dict(engine="python") @@ -147,7 +193,7 @@ def test_read_clipboard_infer_excel(self): 1 2 4 Harry Carney """.strip()) - clipboard_set(text) + mock_clipboard[request.node.name] = text df = pd.read_clipboard(**clip_kwargs) # excel data is parsed correctly @@ -159,7 +205,7 @@ def test_read_clipboard_infer_excel(self): 1 2 3 4 """.strip()) - clipboard_set(text) + mock_clipboard[request.node.name] = text res = pd.read_clipboard(**clip_kwargs) text = dedent(""" @@ -167,7 +213,7 @@ def test_read_clipboard_infer_excel(self): 1 2 3 4 """.strip()) - clipboard_set(text) + mock_clipboard[request.node.name] = text exp = pd.read_clipboard(**clip_kwargs) tm.assert_frame_equal(res, exp) diff --git a/setup.cfg b/setup.cfg index 5fc0236066b93..021159bad99de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ markers = slow: mark a test as slow network: mark a test as network 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 From 6ecb94210a5b25dcee790d093a96d2ea9cb77ac4 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Mon, 17 Sep 2018 18:10:25 +0100 Subject: [PATCH 02/87] removing superfluous reference to axis in Series.reorder_levels docstring (#22734) --- pandas/core/series.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/series.py b/pandas/core/series.py index a4d403e4bcd94..ba34a3e95e5d3 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2886,7 +2886,6 @@ def reorder_levels(self, order): ---------- order : list of int representing new level order. (reference level by number or key) - axis : where to reorder levels Returns ------- From 9e2039bad0112436e3d2adda721d40bb773f5a48 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Mon, 17 Sep 2018 12:10:55 -0700 Subject: [PATCH 03/87] CLN/DOC: Refactor timeseries.rst intro and overview (#22728) * CLN/DOC: Refactor timeseries.rst intro and overview * Address review * Forgot missing is --- doc/source/timeseries.rst | 117 ++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 36 deletions(-) diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 5dfac98d069e7..71bc064ffb0c2 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -21,51 +21,59 @@ Time Series / Date functionality ******************************** -pandas has proven very successful as a tool for working with time series data, -especially in the financial data analysis space. Using the NumPy ``datetime64`` and ``timedelta64`` dtypes, -we have consolidated a large number of features from other Python libraries like ``scikits.timeseries`` as well as created +pandas contains extensive capabilities and features for working with time series data for all domains. +Using the NumPy ``datetime64`` and ``timedelta64`` dtypes, pandas has consolidated a large number of +features from other Python libraries like ``scikits.timeseries`` as well as created a tremendous amount of new functionality for manipulating time series data. -In working with time series data, we will frequently seek to: +For example, pandas supports: -* generate sequences of fixed-frequency dates and time spans -* conform or convert time series to a particular frequency -* compute "relative" dates based on various non-standard time increments - (e.g. 5 business days before the last business day of the year), or "roll" - dates forward or backward +Parsing time series information from various sources and formats -pandas provides a relatively compact and self-contained set of tools for -performing the above tasks. +.. ipython:: python + + dti = pd.to_datetime(['1/1/2018', np.datetime64('2018-01-01'), datetime(2018, 1, 1)]) + dti -Create a range of dates: +Generate sequences of fixed-frequency dates and time spans .. ipython:: python - # 72 hours starting with midnight Jan 1st, 2011 - rng = pd.date_range('1/1/2011', periods=72, freq='H') - rng[:5] + dti = pd.date_range('2018-01-01', periods=3, freq='H') + dti -Index pandas objects with dates: +Manipulating and converting date times with timezone information .. ipython:: python - ts = pd.Series(np.random.randn(len(rng)), index=rng) - ts.head() + dti = dti.tz_localize('UTC') + dti + dti.tz_convert('US/Pacific') -Change frequency and fill gaps: +Resampling or converting a time series to a particular frequency .. ipython:: python - # to 45 minute frequency and forward fill - converted = ts.asfreq('45Min', method='pad') - converted.head() + idx = pd.date_range('2018-01-01', periods=5, freq='H') + ts = pd.Series(range(len(idx)), index=idx) + ts + ts.resample('2H').mean() -Resample the series to a daily frequency: +Performing date and time arithmetic with absolute or relative time increments .. ipython:: python - # Daily means - ts.resample('D').mean() + friday = pd.Timestamp('2018-01-05') + friday.day_name() + # Add 1 day + saturday = friday + pd.Timedelta('1 day') + saturday.day_name() + # Add 1 business day (Friday --> Monday) + monday = friday + pd.tseries.offsets.BDay() + monday.day_name() + +pandas provides a relatively compact and self-contained set of tools for +performing the above tasks and more. .. _timeseries.overview: @@ -73,17 +81,54 @@ Resample the series to a daily frequency: Overview -------- -The following table shows the type of time-related classes pandas can handle and -how to create them. +pandas captures 4 general time related concepts: + +#. Date times: A specific date and time with timezone support. Similar to ``datetime.datetime`` from the standard library. +#. Time deltas: An absolute time duration. Similar to ``datetime.timedelta`` from the standard library. +#. Time spans: A span of time defined by a point in time and its associated frequency. +#. Date offsets: A relative time duration that respects calendar arithmetic. Similar to ``dateutil.relativedelta.relativedelta`` from the ``dateutil`` package. -================= =============================== =================================================================== -Class Remarks How to create -================= =============================== =================================================================== -``Timestamp`` Represents a single timestamp ``to_datetime``, ``Timestamp`` -``DatetimeIndex`` Index of ``Timestamp`` ``to_datetime``, ``date_range``, ``bdate_range``, ``DatetimeIndex`` -``Period`` Represents a single time span ``Period`` -``PeriodIndex`` Index of ``Period`` ``period_range``, ``PeriodIndex`` -================= =============================== =================================================================== +===================== ================= =================== ============================================ ======================================== +Concept Scalar Class Array Class pandas Data Type Primary Creation Method +===================== ================= =================== ============================================ ======================================== +Date times ``Timestamp`` ``DatetimeIndex`` ``datetime64[ns]`` or ``datetime64[ns, tz]`` ``to_datetime`` or ``date_range`` +Time deltas ``Timedelta`` ``TimedeltaIndex`` ``timedelta64[ns]`` ``to_timedelta`` or ``timedelta_range`` +Time spans ``Period`` ``PeriodIndex`` ``period[freq]`` ``Period`` or ``period_range`` +Date offsets ``DateOffset`` ``None`` ``None`` ``DateOffset`` +===================== ================= =================== ============================================ ======================================== + +For time series data, it's conventional to represent the time component in the index of a :class:`Series` or :class:`DataFrame` +so manipulations can be performed with respect to the time element. + +.. ipython:: python + + pd.Series(range(3), index=pd.date_range('2000', freq='D', periods=3)) + +However, :class:`Series` and :class:`DataFrame` can directly also support the time component as data itself. + +.. ipython:: python + + pd.Series(pd.date_range('2000', freq='D', periods=3)) + +:class:`Series` and :class:`DataFrame` have extended data type support and functionality for ``datetime`` and ``timedelta`` +data when the time data is used as data itself. The ``Period`` and ``DateOffset`` data will be stored as ``object`` data. + +.. ipython:: python + + pd.Series(pd.period_range('1/1/2011', freq='M', periods=3)) + pd.Series(pd.date_range('1/1/2011', freq='M', periods=3)) + +Lastly, pandas represents null date times, time deltas, and time spans as ``NaT`` which +is useful for representing missing or null date like values and behaves similar +as ``np.nan`` does for float data. + +.. ipython:: python + + pd.Timestamp(pd.NaT) + pd.Timedelta(pd.NaT) + pd.Period(pd.NaT) + # Equality acts as np.nan would + pd.NaT == pd.NaT .. _timeseries.representation: @@ -1443,7 +1488,7 @@ time. The method for this is :meth:`~Series.shift`, which is available on all of the pandas objects. .. ipython:: python - + ts = pd.Series(range(len(rng)), index=rng) ts = ts[:5] ts.shift(1) From 249f0ee72a7c6bc6260423c8293f62e44dd4ff1a Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Tue, 18 Sep 2018 04:10:56 -0700 Subject: [PATCH 04/87] CLN: Remove unused imports in pyx files (#22739) --- pandas/_libs/sparse.pyx | 3 --- pandas/_libs/tslib.pyx | 2 -- 2 files changed, 5 deletions(-) diff --git a/pandas/_libs/sparse.pyx b/pandas/_libs/sparse.pyx index 2993114a668bb..d852711d3b707 100644 --- a/pandas/_libs/sparse.pyx +++ b/pandas/_libs/sparse.pyx @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -import operator -import sys - import cython import numpy as np diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 16fea0615f199..9012ebefe0975 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -15,8 +15,6 @@ import numpy as np cnp.import_array() import pytz -from dateutil.tz import tzlocal, tzutc as dateutil_utc - from util cimport (is_integer_object, is_float_object, is_string_object, is_datetime64_object) From 9e99299cb6df0d1e028353f9de550410fd3bfce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vit=C3=B3ria=20Helena?= Date: Tue, 18 Sep 2018 08:15:24 -0300 Subject: [PATCH 05/87] CLN: Removes module pandas.json (#22737) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/__init__.py | 3 --- pandas/json.py | 7 ------- pandas/tests/api/test_api.py | 9 +-------- 4 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 pandas/json.py diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 649629714c3b1..34eb5d8d7ed0f 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -577,6 +577,7 @@ Removal of prior version deprecations/changes - Removed the ``pandas.formats.style`` shim for :class:`pandas.io.formats.style.Styler` (:issue:`16059`) - :meth:`Categorical.searchsorted` and :meth:`Series.searchsorted` have renamed the ``v`` argument to ``value`` (:issue:`14645`) - :meth:`TimedeltaIndex.searchsorted`, :meth:`DatetimeIndex.searchsorted`, and :meth:`PeriodIndex.searchsorted` have renamed the ``key`` argument to ``value`` (:issue:`14645`) +- Removal of the previously deprecated module ``pandas.json`` (:issue:`19944`) .. _whatsnew_0240.performance: diff --git a/pandas/__init__.py b/pandas/__init__.py index 97ae73174c09c..f91d0aa84e0ff 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -61,9 +61,6 @@ # extension module deprecations from pandas.util._depr_module import _DeprecatedModule -json = _DeprecatedModule(deprmod='pandas.json', - moved={'dumps': 'pandas.io.json.dumps', - 'loads': 'pandas.io.json.loads'}) parser = _DeprecatedModule(deprmod='pandas.parser', removals=['na_values'], moved={'CParserError': 'pandas.errors.ParserError'}) diff --git a/pandas/json.py b/pandas/json.py deleted file mode 100644 index 16d6580c87951..0000000000000 --- a/pandas/json.py +++ /dev/null @@ -1,7 +0,0 @@ -# flake8: noqa - -import warnings -warnings.warn("The pandas.json module is deprecated and will be " - "removed in a future version. Please import from " - "pandas.io.json instead", FutureWarning, stacklevel=2) -from pandas._libs.json import dumps, loads diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index bf9e14b427015..199700b304a4e 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -35,7 +35,7 @@ class TestPDApi(Base): 'util', 'options', 'io'] # these are already deprecated; awaiting removal - deprecated_modules = ['parser', 'json', 'lib', 'tslib'] + deprecated_modules = ['parser', 'lib', 'tslib'] # misc misc = ['IndexSlice', 'NaT'] @@ -173,13 +173,6 @@ def test_get_store(self): s.close() -class TestJson(object): - - def test_deprecation_access_func(self): - with catch_warnings(record=True): - pd.json.dumps([]) - - class TestParser(object): def test_deprecation_access_func(self): From 712c8b1067a348d519cffba910419c0848740a37 Mon Sep 17 00:00:00 2001 From: Simon Hawkins Date: Tue, 18 Sep 2018 12:17:20 +0100 Subject: [PATCH 06/87] TST/CLN: remove duplicate data file used in tests (unicode_series.csv) (#22723) --- .../tests/io/formats/data/unicode_series.csv | 18 ------------------ pandas/tests/io/formats/test_format.py | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 pandas/tests/io/formats/data/unicode_series.csv diff --git a/pandas/tests/io/formats/data/unicode_series.csv b/pandas/tests/io/formats/data/unicode_series.csv deleted file mode 100644 index 2485e149edb06..0000000000000 --- a/pandas/tests/io/formats/data/unicode_series.csv +++ /dev/null @@ -1,18 +0,0 @@ -1617,King of New York (1990) -1618,All Things Fair (1996) -1619,"Sixth Man, The (1997)" -1620,Butterfly Kiss (1995) -1621,"Paris, France (1993)" -1622,"Cérémonie, La (1995)" -1623,Hush (1998) -1624,Nightwatch (1997) -1625,Nobody Loves Me (Keiner liebt mich) (1994) -1626,"Wife, The (1995)" -1627,Lamerica (1994) -1628,Nico Icon (1995) -1629,"Silence of the Palace, The (Saimt el Qusur) (1994)" -1630,"Slingshot, The (1993)" -1631,Land and Freedom (Tierra y libertad) (1995) -1632,Á köldum klaka (Cold Fever) (1994) -1633,Etz Hadomim Tafus (Under the Domin Tree) (1994) -1634,Two Friends (1986) diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index c19f8e57f9ae7..ffbc978b92ba5 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -955,7 +955,7 @@ def test_unicode_problem_decoding_as_ascii(self): compat.text_type(dm.to_string()) def test_string_repr_encoding(self, datapath): - filepath = datapath('io', 'formats', 'data', 'unicode_series.csv') + filepath = datapath('io', 'parser', 'data', 'unicode_series.csv') df = pd.read_csv(filepath, header=None, encoding='latin1') repr(df) repr(df[1]) From c6f7e860ecc82d69b6855382ee642a22c67acd5c Mon Sep 17 00:00:00 2001 From: Troels Nielsen Date: Tue, 18 Sep 2018 14:13:45 +0200 Subject: [PATCH 07/87] BUG: Some sas7bdat files with many columns are not parseable by read_sas (#22628) --- doc/source/whatsnew/v0.24.0.txt | 2 + pandas/io/sas/sas.pyx | 10 +-- pandas/io/sas/sas7bdat.py | 61 ++++++++++-------- 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/test_sas7bdat.py | 16 +++++ 7 files changed, 62 insertions(+), 31 deletions(-) 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 diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 34eb5d8d7ed0f..cccbe47073fbd 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -743,6 +743,8 @@ I/O - :func:`read_excel()` will correctly show the deprecation warning for previously deprecated ``sheetname`` (:issue:`17994`) - :func:`read_csv()` will correctly parse timezone-aware datetimes (:issue:`22256`) - :func:`read_sas()` will parse numbers in sas7bdat-files that have width less than 8 bytes correctly. (:issue:`21616`) +- :func:`read_sas()` will correctly parse sas7bdat files with many columns (:issue:`22628`) +- :func:`read_sas()` will correctly parse sas7bdat files with data page types having also bit 7 set (so page type is 128 + 256 = 384) (:issue:`16615`) Plotting ^^^^^^^^ diff --git a/pandas/io/sas/sas.pyx b/pandas/io/sas/sas.pyx index 221c07a0631d2..a5bfd5866a261 100644 --- a/pandas/io/sas/sas.pyx +++ b/pandas/io/sas/sas.pyx @@ -244,8 +244,8 @@ cdef class Parser(object): self.parser = parser self.header_length = self.parser.header_length self.column_count = parser.column_count - self.lengths = parser._column_data_lengths - self.offsets = parser._column_data_offsets + self.lengths = parser.column_data_lengths() + self.offsets = parser.column_data_offsets() self.byte_chunk = parser._byte_chunk self.string_chunk = parser._string_chunk self.row_length = parser.row_length @@ -257,7 +257,7 @@ cdef class Parser(object): # page indicators self.update_next_page() - column_types = parser.column_types + column_types = parser.column_types() # map column types for j in range(self.column_count): @@ -375,7 +375,7 @@ cdef class Parser(object): if done: return True return False - elif self.current_page_type == page_data_type: + elif self.current_page_type & page_data_type == page_data_type: self.process_byte_array_with_data( bit_offset + subheader_pointers_offset + self.current_row_on_page_index * self.row_length, @@ -437,7 +437,7 @@ cdef class Parser(object): elif column_types[j] == column_type_string: # string string_chunk[js, current_row] = np.array(source[start:( - start + lngt)]).tostring().rstrip() + start + lngt)]).tostring().rstrip(b"\x00 ") js += 1 self.current_row_on_page_index += 1 diff --git a/pandas/io/sas/sas7bdat.py b/pandas/io/sas/sas7bdat.py index efeb306b618d1..3582f538c16bf 100644 --- a/pandas/io/sas/sas7bdat.py +++ b/pandas/io/sas/sas7bdat.py @@ -82,7 +82,6 @@ def __init__(self, path_or_buf, index=None, convert_dates=True, self.compression = "" self.column_names_strings = [] self.column_names = [] - self.column_types = [] self.column_formats = [] self.columns = [] @@ -90,6 +89,8 @@ def __init__(self, path_or_buf, index=None, convert_dates=True, self._cached_page = None self._column_data_lengths = [] self._column_data_offsets = [] + self._column_types = [] + self._current_row_in_file_index = 0 self._current_row_on_page_index = 0 self._current_row_in_file_index = 0 @@ -102,6 +103,19 @@ def __init__(self, path_or_buf, index=None, convert_dates=True, self._get_properties() self._parse_metadata() + def column_data_lengths(self): + """Return a numpy int64 array of the column data lengths""" + return np.asarray(self._column_data_lengths, dtype=np.int64) + + def column_data_offsets(self): + """Return a numpy int64 array of the column offsets""" + return np.asarray(self._column_data_offsets, dtype=np.int64) + + def column_types(self): + """Returns a numpy character array of the column types: + s (string) or d (double)""" + return np.asarray(self._column_types, dtype=np.dtype('S1')) + def close(self): try: self.handle.close() @@ -287,8 +301,10 @@ def _process_page_meta(self): pt = [const.page_meta_type, const.page_amd_type] + const.page_mix_types if self._current_page_type in pt: self._process_page_metadata() - return ((self._current_page_type in [256] + const.page_mix_types) or - (self._current_page_data_subheader_pointers is not None)) + is_data_page = self._current_page_type & const.page_data_type + is_mix_page = self._current_page_type in const.page_mix_types + return (is_data_page or is_mix_page + or self._current_page_data_subheader_pointers != []) def _read_page_header(self): bit_offset = self._page_bit_offset @@ -503,12 +519,6 @@ def _process_columnattributes_subheader(self, offset, length): int_len = self._int_length column_attributes_vectors_count = ( length - 2 * int_len - 12) // (int_len + 8) - self.column_types = np.empty( - column_attributes_vectors_count, dtype=np.dtype('S1')) - self._column_data_lengths = np.empty( - column_attributes_vectors_count, dtype=np.int64) - self._column_data_offsets = np.empty( - column_attributes_vectors_count, dtype=np.int64) for i in range(column_attributes_vectors_count): col_data_offset = (offset + int_len + const.column_data_offset_offset + @@ -520,16 +530,13 @@ def _process_columnattributes_subheader(self, offset, length): const.column_type_offset + i * (int_len + 8)) x = self._read_int(col_data_offset, int_len) - self._column_data_offsets[i] = x + self._column_data_offsets.append(x) x = self._read_int(col_data_len, const.column_data_length_length) - self._column_data_lengths[i] = x + self._column_data_lengths.append(x) x = self._read_int(col_types, const.column_type_length) - if x == 1: - self.column_types[i] = b'd' - else: - self.column_types[i] = b's' + self._column_types.append(b'd' if x == 1 else b's') def _process_columnlist_subheader(self, offset, length): # unknown purpose @@ -586,7 +593,7 @@ def _process_format_subheader(self, offset, length): col.name = self.column_names[current_column_number] col.label = column_label col.format = column_format - col.ctype = self.column_types[current_column_number] + col.ctype = self._column_types[current_column_number] col.length = self._column_data_lengths[current_column_number] self.column_formats.append(column_format) @@ -599,7 +606,7 @@ def read(self, nrows=None): elif nrows is None: nrows = self.row_count - if len(self.column_types) == 0: + if len(self._column_types) == 0: self.close() raise EmptyDataError("No columns to parse from file") @@ -610,8 +617,8 @@ def read(self, nrows=None): if nrows > m: nrows = m - nd = (self.column_types == b'd').sum() - ns = (self.column_types == b's').sum() + nd = self._column_types.count(b'd') + ns = self._column_types.count(b's') self._string_chunk = np.empty((ns, nrows), dtype=np.object) self._byte_chunk = np.zeros((nd, 8 * nrows), dtype=np.uint8) @@ -639,11 +646,13 @@ def _read_next_page(self): self._page_length)) self._read_page_header() - if self._current_page_type == const.page_meta_type: + page_type = self._current_page_type + if page_type == const.page_meta_type: self._process_page_metadata() - pt = [const.page_meta_type, const.page_data_type] - pt += [const.page_mix_types] - if self._current_page_type not in pt: + + is_data_page = page_type & const.page_data_type + pt = [const.page_meta_type] + const.page_mix_types + if not is_data_page and self._current_page_type not in pt: return self._read_next_page() return False @@ -660,7 +669,7 @@ def _chunk_to_dataframe(self): name = self.column_names[j] - if self.column_types[j] == b'd': + if self._column_types[j] == b'd': rslt[name] = self._byte_chunk[jb, :].view( dtype=self.byte_order + 'd') rslt[name] = np.asarray(rslt[name], dtype=np.float64) @@ -674,7 +683,7 @@ def _chunk_to_dataframe(self): rslt[name] = pd.to_datetime(rslt[name], unit=unit, origin="1960-01-01") jb += 1 - elif self.column_types[j] == b's': + elif self._column_types[j] == b's': rslt[name] = self._string_chunk[js, :] if self.convert_text and (self.encoding is not None): rslt[name] = rslt[name].str.decode( @@ -686,6 +695,6 @@ def _chunk_to_dataframe(self): else: self.close() raise ValueError("unknown column type %s" % - self.column_types[j]) + self._column_types[j]) return rslt diff --git a/pandas/tests/io/sas/data/load_log.sas7bdat b/pandas/tests/io/sas/data/load_log.sas7bdat new file mode 100644 index 0000000000000000000000000000000000000000..dc78925471baf4cc3dea8d568d27c59327c7d578 GIT binary patch literal 589824 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 Date: Tue, 18 Sep 2018 13:18:08 +0100 Subject: [PATCH 08/87] DOC: improve doc string for .aggregate and .transform (#22641) --- ci/doctests.sh | 4 +- pandas/core/frame.py | 7 ++- pandas/core/generic.py | 102 +++++++++++++++++++++++++---------------- pandas/core/series.py | 12 +++-- 4 files changed, 76 insertions(+), 49 deletions(-) diff --git a/ci/doctests.sh b/ci/doctests.sh index 2af5dbd26aeb1..654bd57107904 100755 --- a/ci/doctests.sh +++ b/ci/doctests.sh @@ -21,7 +21,7 @@ if [ "$DOCTEST" ]; then # DataFrame / Series docstrings pytest --doctest-modules -v pandas/core/frame.py \ - -k"-assign -axes -combine -isin -itertuples -join -nlargest -nsmallest -nunique -pivot_table -quantile -query -reindex -reindex_axis -replace -round -set_index -stack -to_dict -to_stata -transform" + -k"-assign -axes -combine -isin -itertuples -join -nlargest -nsmallest -nunique -pivot_table -quantile -query -reindex -reindex_axis -replace -round -set_index -stack -to_dict -to_stata" if [ $? -ne "0" ]; then RET=1 @@ -35,7 +35,7 @@ if [ "$DOCTEST" ]; then fi pytest --doctest-modules -v pandas/core/generic.py \ - -k"-_set_axis_name -_xs -describe -droplevel -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -resample -sample -to_json -to_xarray -transform -transpose -values -xs" + -k"-_set_axis_name -_xs -describe -droplevel -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -resample -sample -to_json -to_xarray -transpose -values -xs" if [ $? -ne "0" ]; then RET=1 diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 251bc6587872d..bb08d4fa5582b 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -109,10 +109,9 @@ _shared_doc_kwargs = dict( axes='index, columns', klass='DataFrame', axes_single_arg="{0 or 'index', 1 or 'columns'}", - axis=""" - axis : {0 or 'index', 1 or 'columns'}, default 0 - - 0 or 'index': apply function to each column. - - 1 or 'columns': apply function to each row.""", + axis="""axis : {0 or 'index', 1 or 'columns'}, default 0 + If 0 or 'index': apply function to each column. + If 1 or 'columns': apply function to each row.""", optional_by=""" by : str or list of str Name or list of names to sort by. diff --git a/pandas/core/generic.py b/pandas/core/generic.py index cdc5b4310bce2..96a956764ce06 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -4545,17 +4545,16 @@ def pipe(self, func, *args, **kwargs): Parameters ---------- - func : function, string, dictionary, or list of string/functions + func : function, str, list or dict Function to use for aggregating the data. If a function, must either - work when passed a %(klass)s or when passed to %(klass)s.apply. For - a DataFrame, can pass a dict, if the keys are DataFrame column names. + work when passed a %(klass)s or when passed to %(klass)s.apply. Accepted combinations are: - - string function name. - - function. - - list of functions. - - dict of column names -> functions (or list of functions). + - function + - string function name + - list of functions and/or function names, e.g. ``[np.sum, 'mean']`` + - dict of axis labels -> functions, function names or list of such. %(axis)s *args Positional arguments to pass to `func`. @@ -4564,7 +4563,11 @@ def pipe(self, func, *args, **kwargs): Returns ------- - aggregated : %(klass)s + DataFrame, Series or scalar + if DataFrame.agg is called with a single function, returns a Series + if DataFrame.agg is called with several functions, returns a DataFrame + if Series.agg is called with single function, returns a scalar + if Series.agg is called with several functions, returns a Series Notes ----- @@ -4574,50 +4577,71 @@ def pipe(self, func, *args, **kwargs): """) _shared_docs['transform'] = (""" - Call function producing a like-indexed %(klass)s - and return a %(klass)s with the transformed values + Call ``func`` on self producing a %(klass)s with transformed values + and that has the same axis length as self. .. versionadded:: 0.20.0 Parameters ---------- - func : callable, string, dictionary, or list of string/callables - To apply to column + func : function, str, list or dict + Function to use for transforming the data. If a function, must either + work when passed a %(klass)s or when passed to %(klass)s.apply. - Accepted Combinations are: + Accepted combinations are: - - string function name - function - - list of functions - - dict of column names -> functions (or list of functions) + - string function name + - list of functions and/or function names, e.g. ``[np.exp. 'sqrt']`` + - dict of axis labels -> functions, function names or list of such. + %(axis)s + *args + Positional arguments to pass to `func`. + **kwargs + Keyword arguments to pass to `func`. Returns ------- - transformed : %(klass)s + %(klass)s + A %(klass)s that must have the same length as self. - Examples + Raises + ------ + ValueError : If the returned %(klass)s has a different length than self. + + See Also -------- - >>> df = pd.DataFrame(np.random.randn(10, 3), columns=['A', 'B', 'C'], - ... index=pd.date_range('1/1/2000', periods=10)) - df.iloc[3:7] = np.nan - - >>> df.transform(lambda x: (x - x.mean()) / x.std()) - A B C - 2000-01-01 0.579457 1.236184 0.123424 - 2000-01-02 0.370357 -0.605875 -1.231325 - 2000-01-03 1.455756 -0.277446 0.288967 - 2000-01-04 NaN NaN NaN - 2000-01-05 NaN NaN NaN - 2000-01-06 NaN NaN NaN - 2000-01-07 NaN NaN NaN - 2000-01-08 -0.498658 1.274522 1.642524 - 2000-01-09 -0.540524 -1.012676 -0.828968 - 2000-01-10 -1.366388 -0.614710 0.005378 - - See also + %(klass)s.agg : Only perform aggregating type operations. + %(klass)s.apply : Invoke function on a %(klass)s. + + Examples -------- - pandas.%(klass)s.aggregate - pandas.%(klass)s.apply + >>> df = pd.DataFrame({'A': range(3), 'B': range(1, 4)}) + >>> df + A B + 0 0 1 + 1 1 2 + 2 2 3 + >>> df.transform(lambda x: x + 1) + A B + 0 1 2 + 1 2 3 + 2 3 4 + + Even though the resulting %(klass)s must have the same length as the + input %(klass)s, it is possible to provide several input functions: + + >>> s = pd.Series(range(3)) + >>> s + 0 0 + 1 1 + 2 2 + dtype: int64 + >>> s.transform([np.sqrt, np.exp]) + sqrt exp + 0 0.000000 1.000000 + 1 1.000000 2.718282 + 2 1.414214 7.389056 """) # ---------------------------------------------------------------------- @@ -9401,7 +9425,7 @@ def ewm(self, com=None, span=None, halflife=None, alpha=None, cls.ewm = ewm - @Appender(_shared_docs['transform'] % _shared_doc_kwargs) + @Appender(_shared_docs['transform'] % dict(axis="", **_shared_doc_kwargs)) def transform(self, func, *args, **kwargs): result = self.agg(func, *args, **kwargs) if is_scalar(result) or len(result) != len(self): diff --git a/pandas/core/series.py b/pandas/core/series.py index ba34a3e95e5d3..0268b8e9c3149 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -89,10 +89,8 @@ _shared_doc_kwargs = dict( axes='index', klass='Series', axes_single_arg="{0 or 'index'}", - axis=""" - axis : {0 or 'index'} - Parameter needed for compatibility with DataFrame. - """, + axis="""axis : {0 or 'index'} + Parameter needed for compatibility with DataFrame.""", inplace="""inplace : boolean, default False If True, performs operation inplace and returns None.""", unique='np.ndarray', duplicated='Series', @@ -3097,6 +3095,12 @@ def aggregate(self, func, axis=0, *args, **kwargs): agg = aggregate + @Appender(generic._shared_docs['transform'] % _shared_doc_kwargs) + def transform(self, func, axis=0, *args, **kwargs): + # Validate the axis parameter + self._get_axis_number(axis) + return super(Series, self).transform(func, *args, **kwargs) + def apply(self, func, convert_dtype=True, args=(), **kwds): """ Invoke function on values of Series. Can be ufunc (a NumPy function From 33f38b959118e1e97016d4ddf68555ce476c480f Mon Sep 17 00:00:00 2001 From: Hannah Ferchland <32065449+HannahFerch@users.noreply.github.com> Date: Tue, 18 Sep 2018 14:28:59 +0200 Subject: [PATCH 09/87] BUG: DataFrame.apply not adding a frequency if freq=None (#22150) (#22561) --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/core/indexes/datetimes.py | 2 -- pandas/tests/frame/test_apply.py | 23 +++++++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index cccbe47073fbd..8ae7f06352510 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -636,7 +636,7 @@ Datetimelike - Bug in :meth:`DataFrame.eq` comparison against ``NaT`` incorrectly returning ``True`` or ``NaN`` (:issue:`15697`, :issue:`22163`) - Bug in :class:`DatetimeIndex` subtraction that incorrectly failed to raise ``OverflowError`` (:issue:`22492`, :issue:`22508`) - Bug in :class:`DatetimeIndex` incorrectly allowing indexing with ``Timedelta`` object (:issue:`20464`) -- +- Bug in :class:`DatetimeIndex` where frequency was being set if original frequency was ``None`` (:issue:`22150`) Timedelta ^^^^^^^^^ diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 46741ab15aa31..9b00f21668bf5 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -860,8 +860,6 @@ def union_many(self, others): if isinstance(this, DatetimeIndex): this._tz = timezones.tz_standardize(tz) - if this.freq is None: - this.freq = to_offset(this.inferred_freq) return this def join(self, other, how='left', level=None, return_indexers=False, diff --git a/pandas/tests/frame/test_apply.py b/pandas/tests/frame/test_apply.py index 8beab3fb816df..1452e1ab8d98d 100644 --- a/pandas/tests/frame/test_apply.py +++ b/pandas/tests/frame/test_apply.py @@ -11,6 +11,8 @@ import warnings import numpy as np +from hypothesis import given +from hypothesis.strategies import composite, dates, integers, sampled_from from pandas import (notna, DataFrame, Series, MultiIndex, date_range, Timestamp, compat) @@ -1155,3 +1157,24 @@ def test_agg_cython_table_raises(self, df, func, expected, axis): # GH21224 with pytest.raises(expected): df.agg(func, axis=axis) + + @composite + def indices(draw, max_length=5): + date = draw( + dates( + min_value=Timestamp.min.ceil("D").to_pydatetime().date(), + max_value=Timestamp.max.floor("D").to_pydatetime().date(), + ).map(Timestamp) + ) + periods = draw(integers(0, max_length)) + freq = draw(sampled_from(list("BDHTS"))) + dr = date_range(date, periods=periods, freq=freq) + return pd.DatetimeIndex(list(dr)) + + @given(index=indices(5), num_columns=integers(0, 5)) + def test_frequency_is_original(self, index, num_columns): + # GH22150 + original = index.copy() + df = DataFrame(True, index=index, columns=range(num_columns)) + df.apply(lambda x: x) + assert index.freq == original.freq From 1542d29ab5b0acda9a98d080715007cee775df10 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 18 Sep 2018 05:29:48 -0700 Subject: [PATCH 10/87] [ENH] pull in warning for dialect change from pandas-gbq. (#22557) --- doc/source/whatsnew/v0.24.0.txt | 6 +++--- pandas/io/gbq.py | 19 ++++++++++++++++++- pandas/tests/io/test_gbq.py | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 8ae7f06352510..1dd8bd401face 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -170,9 +170,9 @@ Other Enhancements - :meth:`Series.droplevel` and :meth:`DataFrame.droplevel` are now implemented (:issue:`20342`) - Added support for reading from Google Cloud Storage via the ``gcsfs`` library (:issue:`19454`) - :func:`to_gbq` and :func:`read_gbq` signature and documentation updated to - reflect changes from the `Pandas-GBQ library version 0.5.0 - `__. - (:issue:`21627`) + reflect changes from the `Pandas-GBQ library version 0.6.0 + `__. + (:issue:`21627`, :issue:`22557`) - New method :meth:`HDFStore.walk` will recursively walk the group hierarchy of an HDF5 file (:issue:`10932`) - :func:`read_html` copies cell data across ``colspan`` and ``rowspan``, and it treats all-``th`` table rows as headers if ``header`` kwarg is not given and there is no ``thead`` (:issue:`17054`) - :meth:`Series.nlargest`, :meth:`Series.nsmallest`, :meth:`DataFrame.nlargest`, and :meth:`DataFrame.nsmallest` now accept the value ``"all"`` for the ``keep`` argument. This keeps all ties for the nth largest/smallest value (:issue:`16818`) diff --git a/pandas/io/gbq.py b/pandas/io/gbq.py index 87a0e4d5d1747..46e1b13631f07 100644 --- a/pandas/io/gbq.py +++ b/pandas/io/gbq.py @@ -1,5 +1,7 @@ """ Google BigQuery support """ +import warnings + def _try_import(): # since pandas is a dependency of pandas-gbq @@ -23,7 +25,7 @@ def _try_import(): def read_gbq(query, project_id=None, index_col=None, col_order=None, reauth=False, private_key=None, auth_local_webserver=False, - dialect='legacy', location=None, configuration=None, + dialect=None, location=None, configuration=None, verbose=None): """ Load data from Google BigQuery. @@ -65,6 +67,8 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None, *New in version 0.2.0 of pandas-gbq*. dialect : str, default 'legacy' + Note: The default value is changing to 'standard' in a future verion. + SQL syntax dialect to use. Value can be one of: ``'legacy'`` @@ -76,6 +80,8 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None, compliant with the SQL 2011 standard. For more information see `BigQuery Standard SQL Reference `__. + + .. versionchanged:: 0.24.0 location : str, optional Location where the query job should run. See the `BigQuery locations documentation @@ -108,6 +114,17 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None, pandas.DataFrame.to_gbq : Write a DataFrame to Google BigQuery. """ pandas_gbq = _try_import() + + if dialect is None: + dialect = "legacy" + warnings.warn( + 'The default value for dialect is changing to "standard" in a ' + 'future version of pandas-gbq. Pass in dialect="legacy" to ' + "disable this warning.", + FutureWarning, + stacklevel=2, + ) + return pandas_gbq.read_gbq( query, project_id=project_id, index_col=index_col, col_order=col_order, reauth=reauth, verbose=verbose, diff --git a/pandas/tests/io/test_gbq.py b/pandas/tests/io/test_gbq.py index dc6c319bb3366..68413d610e615 100644 --- a/pandas/tests/io/test_gbq.py +++ b/pandas/tests/io/test_gbq.py @@ -4,11 +4,17 @@ import platform import os +try: + from unittest import mock +except ImportError: + mock = pytest.importorskip("mock") + import numpy as np import pandas as pd from pandas import compat, DataFrame - from pandas.compat import range +import pandas.util.testing as tm + pandas_gbq = pytest.importorskip('pandas_gbq') @@ -93,6 +99,16 @@ 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 + mock_read_gbq = mock.Mock() + mock_read_gbq.return_value = 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(object): From 9dd454e607c88a1a379328fe0fee8f41f822e9f1 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Tue, 18 Sep 2018 14:40:45 +0200 Subject: [PATCH 11/87] DOC: Updating str_repeat docstring (#22571) --- pandas/core/strings.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index 08709d15c48bf..b46c6a4557ff3 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -678,20 +678,42 @@ def str_replace(arr, pat, repl, n=-1, case=None, flags=0, regex=True): def str_repeat(arr, repeats): """ - Duplicate each string in the Series/Index by indicated number - of times. + Duplicate each string in the Series or Index. Parameters ---------- - repeats : int or array - Same value for all (int) or different value per (array) + repeats : int or sequence of int + Same value for all (int) or different value per (sequence). Returns ------- - repeated : Series/Index of objects + Series or Index of object + Series or Index of repeated string objects specified by + input parameter repeats. + + Examples + -------- + >>> s = pd.Series(['a', 'b', 'c']) + >>> s + 0 a + 1 b + 2 c + + Single int repeats string in Series + + >>> s.str.repeat(repeats=2) + 0 aa + 1 bb + 2 cc + + Sequence of int repeats corresponding string in Series + + >>> s.str.repeat(repeats=[1, 2, 3]) + 0 a + 1 bb + 2 ccc """ if is_scalar(repeats): - def rep(x): try: return compat.binary_type.__mul__(x, repeats) From 6151ba6e9f7d4acb17c509b5454c3263c8b0c24f Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 18 Sep 2018 05:43:14 -0700 Subject: [PATCH 12/87] use fused types for reshape (#22454) --- pandas/_libs/reshape.pyx | 96 +++++++++++++++++++++++++++--- pandas/_libs/reshape_helper.pxi.in | 81 ------------------------- setup.py | 3 +- 3 files changed, 89 insertions(+), 91 deletions(-) delete mode 100644 pandas/_libs/reshape_helper.pxi.in diff --git a/pandas/_libs/reshape.pyx b/pandas/_libs/reshape.pyx index 8d7e314517ed8..9f4e67ca4e256 100644 --- a/pandas/_libs/reshape.pyx +++ b/pandas/_libs/reshape.pyx @@ -1,15 +1,95 @@ # -*- coding: utf-8 -*- -cimport cython -from cython cimport Py_ssize_t +import cython +from cython import Py_ssize_t -import numpy as np -from numpy cimport (ndarray, - int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, +from numpy cimport (int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t, float32_t, float64_t) -cdef double NaN = np.NaN -cdef double nan = NaN +ctypedef fused reshape_t: + uint8_t + uint16_t + uint32_t + uint64_t + int8_t + int16_t + int32_t + int64_t + float32_t + float64_t + object -include "reshape_helper.pxi" + +@cython.wraparound(False) +@cython.boundscheck(False) +def unstack(reshape_t[:, :] values, uint8_t[:] mask, + Py_ssize_t stride, Py_ssize_t length, Py_ssize_t width, + reshape_t[:, :] new_values, uint8_t[:, :] new_mask): + """ + transform long sorted_values to wide new_values + + Parameters + ---------- + values : typed ndarray + mask : boolean ndarray + stride : int + length : int + width : int + new_values : typed ndarray + result array + new_mask : boolean ndarray + result mask + """ + cdef: + Py_ssize_t i, j, w, nulls, s, offset + + if reshape_t is not object: + # evaluated at compile-time + with nogil: + for i in range(stride): + + nulls = 0 + for j in range(length): + + for w in range(width): + + offset = j * width + w + + if mask[offset]: + s = i * width + w + new_values[j, s] = values[offset - nulls, i] + new_mask[j, s] = 1 + else: + nulls += 1 + + else: + # object-dtype, identical to above but we cannot use nogil + for i in range(stride): + + nulls = 0 + for j in range(length): + + for w in range(width): + + offset = j * width + w + + if mask[offset]: + s = i * width + w + new_values[j, s] = values[offset - nulls, i] + new_mask[j, s] = 1 + else: + nulls += 1 + + +unstack_uint8 = unstack["uint8_t"] +unstack_uint16 = unstack["uint16_t"] +unstack_uint32 = unstack["uint32_t"] +unstack_uint64 = unstack["uint64_t"] +unstack_int8 = unstack["int8_t"] +unstack_int16 = unstack["int16_t"] +unstack_int32 = unstack["int32_t"] +unstack_int64 = unstack["int64_t"] +unstack_float32 = unstack["float32_t"] +unstack_float64 = unstack["float64_t"] +unstack_object = unstack["object"] diff --git a/pandas/_libs/reshape_helper.pxi.in b/pandas/_libs/reshape_helper.pxi.in deleted file mode 100644 index bb9a5977f8b45..0000000000000 --- a/pandas/_libs/reshape_helper.pxi.in +++ /dev/null @@ -1,81 +0,0 @@ -""" -Template for each `dtype` helper function for take - -WARNING: DO NOT edit .pxi FILE directly, .pxi is generated from .pxi.in -""" - -# ---------------------------------------------------------------------- -# reshape -# ---------------------------------------------------------------------- - -{{py: - -# name, c_type -dtypes = [('uint8', 'uint8_t'), - ('uint16', 'uint16_t'), - ('uint32', 'uint32_t'), - ('uint64', 'uint64_t'), - ('int8', 'int8_t'), - ('int16', 'int16_t'), - ('int32', 'int32_t'), - ('int64', 'int64_t'), - ('float32', 'float32_t'), - ('float64', 'float64_t'), - ('object', 'object')] -}} - -{{for dtype, c_type in dtypes}} - - -@cython.wraparound(False) -@cython.boundscheck(False) -def unstack_{{dtype}}(ndarray[{{c_type}}, ndim=2] values, - ndarray[uint8_t, ndim=1] mask, - Py_ssize_t stride, - Py_ssize_t length, - Py_ssize_t width, - ndarray[{{c_type}}, ndim=2] new_values, - ndarray[uint8_t, ndim=2] new_mask): - """ - transform long sorted_values to wide new_values - - Parameters - ---------- - values : typed ndarray - mask : boolean ndarray - stride : int - length : int - width : int - new_values : typed ndarray - result array - new_mask : boolean ndarray - result mask - - """ - - cdef: - Py_ssize_t i, j, w, nulls, s, offset - - {{if dtype == 'object'}} - if True: - {{else}} - with nogil: - {{endif}} - - for i in range(stride): - - nulls = 0 - for j in range(length): - - for w in range(width): - - offset = j * width + w - - if mask[offset]: - s = i * width + w - new_values[j, s] = values[offset - nulls, i] - new_mask[j, s] = 1 - else: - nulls += 1 - -{{endfor}} diff --git a/setup.py b/setup.py index 19438d950e8a7..2aca048dcd4fb 100755 --- a/setup.py +++ b/setup.py @@ -77,7 +77,6 @@ def is_platform_windows(): '_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'], 'index': ['_libs/index_class_helper.pxi.in'], @@ -558,7 +557,7 @@ def srcpath(name=None, suffix='.pyx', subdir='src'): 'include': []}, '_libs.reshape': { 'pyxfile': '_libs/reshape', - 'depends': _pxi_dep['reshape']}, + 'depends': []}, '_libs.skiplist': { 'pyxfile': '_libs/skiplist', 'depends': ['pandas/_libs/src/skiplist.h']}, From 8ff8f90a458fb58a4ce81a054a85c907be715f47 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 18 Sep 2018 05:44:59 -0700 Subject: [PATCH 13/87] use fused types for parts of algos_common_helper (#22452) --- pandas/_libs/algos.pyx | 517 ++++++++++++++++++++++++ pandas/_libs/algos_common_helper.pxi.in | 437 -------------------- 2 files changed, 517 insertions(+), 437 deletions(-) diff --git a/pandas/_libs/algos.pyx b/pandas/_libs/algos.pyx index 415e7026e09c8..d2914dc8ac751 100644 --- a/pandas/_libs/algos.pyx +++ b/pandas/_libs/algos.pyx @@ -353,6 +353,523 @@ def nancorr_spearman(ndarray[float64_t, ndim=2] mat, Py_ssize_t minp=1): return result +# ---------------------------------------------------------------------- + +ctypedef fused algos_t: + float64_t + float32_t + object + int32_t + int64_t + uint64_t + uint8_t + + +# TODO: unused; needed? +@cython.wraparound(False) +@cython.boundscheck(False) +cpdef map_indices(ndarray[algos_t] index): + """ + Produce a dict mapping the values of the input array to their respective + locations. + + Example: + array(['hi', 'there']) --> {'hi' : 0 , 'there' : 1} + + Better to do this with Cython because of the enormous speed boost. + """ + cdef: + Py_ssize_t i, length + dict result = {} + + length = len(index) + + for i in range(length): + result[index[i]] = i + + return result + + +@cython.boundscheck(False) +@cython.wraparound(False) +def pad(ndarray[algos_t] old, ndarray[algos_t] new, limit=None): + cdef: + Py_ssize_t i, j, nleft, nright + ndarray[int64_t, ndim=1] indexer + algos_t cur, next + int lim, fill_count = 0 + + nleft = len(old) + nright = len(new) + indexer = np.empty(nright, dtype=np.int64) + indexer.fill(-1) + + if limit is None: + lim = nright + else: + if not util.is_integer_object(limit): + raise ValueError('Limit must be an integer') + if limit < 1: + raise ValueError('Limit must be greater than 0') + lim = limit + + if nleft == 0 or nright == 0 or new[nright - 1] < old[0]: + return indexer + + i = j = 0 + + cur = old[0] + + while j <= nright - 1 and new[j] < cur: + j += 1 + + while True: + if j == nright: + break + + if i == nleft - 1: + while j < nright: + if new[j] == cur: + indexer[j] = i + elif new[j] > cur and fill_count < lim: + indexer[j] = i + fill_count += 1 + j += 1 + break + + next = old[i + 1] + + while j < nright and cur <= new[j] < next: + if new[j] == cur: + indexer[j] = i + elif fill_count < lim: + indexer[j] = i + fill_count += 1 + j += 1 + + fill_count = 0 + i += 1 + cur = next + + return indexer + + +pad_float64 = pad["float64_t"] +pad_float32 = pad["float32_t"] +pad_object = pad["object"] +pad_int64 = pad["int64_t"] +pad_int32 = pad["int32_t"] +pad_uint64 = pad["uint64_t"] +pad_bool = pad["uint8_t"] + + +@cython.boundscheck(False) +@cython.wraparound(False) +def pad_inplace(ndarray[algos_t] values, + ndarray[uint8_t, cast=True] mask, + limit=None): + cdef: + Py_ssize_t i, N + algos_t val + int lim, fill_count = 0 + + N = len(values) + + # GH#2778 + if N == 0: + return + + if limit is None: + lim = N + else: + if not util.is_integer_object(limit): + raise ValueError('Limit must be an integer') + if limit < 1: + raise ValueError('Limit must be greater than 0') + lim = limit + + val = values[0] + for i in range(N): + if mask[i]: + if fill_count >= lim: + continue + fill_count += 1 + values[i] = val + else: + fill_count = 0 + val = values[i] + + +pad_inplace_float64 = pad_inplace["float64_t"] +pad_inplace_float32 = pad_inplace["float32_t"] +pad_inplace_object = pad_inplace["object"] +pad_inplace_int64 = pad_inplace["int64_t"] +pad_inplace_int32 = pad_inplace["int32_t"] +pad_inplace_uint64 = pad_inplace["uint64_t"] +pad_inplace_bool = pad_inplace["uint8_t"] + + +@cython.boundscheck(False) +@cython.wraparound(False) +def pad_2d_inplace(ndarray[algos_t, ndim=2] values, + ndarray[uint8_t, ndim=2] mask, + limit=None): + cdef: + Py_ssize_t i, j, N, K + algos_t val + int lim, fill_count = 0 + + K, N = ( values).shape + + # GH#2778 + if N == 0: + return + + if limit is None: + lim = N + else: + if not util.is_integer_object(limit): + raise ValueError('Limit must be an integer') + if limit < 1: + raise ValueError('Limit must be greater than 0') + lim = limit + + for j in range(K): + fill_count = 0 + val = values[j, 0] + for i in range(N): + if mask[j, i]: + if fill_count >= lim: + continue + fill_count += 1 + values[j, i] = val + else: + fill_count = 0 + val = values[j, i] + + +pad_2d_inplace_float64 = pad_2d_inplace["float64_t"] +pad_2d_inplace_float32 = pad_2d_inplace["float32_t"] +pad_2d_inplace_object = pad_2d_inplace["object"] +pad_2d_inplace_int64 = pad_2d_inplace["int64_t"] +pad_2d_inplace_int32 = pad_2d_inplace["int32_t"] +pad_2d_inplace_uint64 = pad_2d_inplace["uint64_t"] +pad_2d_inplace_bool = pad_2d_inplace["uint8_t"] + + +""" +Backfilling logic for generating fill vector + +Diagram of what's going on + +Old New Fill vector Mask + . 0 1 + . 0 1 + . 0 1 +A A 0 1 + . 1 1 + . 1 1 + . 1 1 + . 1 1 + . 1 1 +B B 1 1 + . 2 1 + . 2 1 + . 2 1 +C C 2 1 + . 0 + . 0 +D +""" + + +@cython.boundscheck(False) +@cython.wraparound(False) +def backfill(ndarray[algos_t] old, ndarray[algos_t] new, limit=None): + cdef: + Py_ssize_t i, j, nleft, nright + ndarray[int64_t, ndim=1] indexer + algos_t cur, prev + int lim, fill_count = 0 + + nleft = len(old) + nright = len(new) + indexer = np.empty(nright, dtype=np.int64) + indexer.fill(-1) + + if limit is None: + lim = nright + else: + if not util.is_integer_object(limit): + raise ValueError('Limit must be an integer') + if limit < 1: + raise ValueError('Limit must be greater than 0') + lim = limit + + if nleft == 0 or nright == 0 or new[0] > old[nleft - 1]: + return indexer + + i = nleft - 1 + j = nright - 1 + + cur = old[nleft - 1] + + while j >= 0 and new[j] > cur: + j -= 1 + + while True: + if j < 0: + break + + if i == 0: + while j >= 0: + if new[j] == cur: + indexer[j] = i + elif new[j] < cur and fill_count < lim: + indexer[j] = i + fill_count += 1 + j -= 1 + break + + prev = old[i - 1] + + while j >= 0 and prev < new[j] <= cur: + if new[j] == cur: + indexer[j] = i + elif new[j] < cur and fill_count < lim: + indexer[j] = i + fill_count += 1 + j -= 1 + + fill_count = 0 + i -= 1 + cur = prev + + return indexer + + +backfill_float64 = backfill["float64_t"] +backfill_float32 = backfill["float32_t"] +backfill_object = backfill["object"] +backfill_int64 = backfill["int64_t"] +backfill_int32 = backfill["int32_t"] +backfill_uint64 = backfill["uint64_t"] +backfill_bool = backfill["uint8_t"] + + +@cython.boundscheck(False) +@cython.wraparound(False) +def backfill_inplace(ndarray[algos_t] values, + ndarray[uint8_t, cast=True] mask, + limit=None): + cdef: + Py_ssize_t i, N + algos_t val + int lim, fill_count = 0 + + N = len(values) + + # GH#2778 + if N == 0: + return + + if limit is None: + lim = N + else: + if not util.is_integer_object(limit): + raise ValueError('Limit must be an integer') + if limit < 1: + raise ValueError('Limit must be greater than 0') + lim = limit + + val = values[N - 1] + for i in range(N - 1, -1, -1): + if mask[i]: + if fill_count >= lim: + continue + fill_count += 1 + values[i] = val + else: + fill_count = 0 + val = values[i] + + +backfill_inplace_float64 = backfill_inplace["float64_t"] +backfill_inplace_float32 = backfill_inplace["float32_t"] +backfill_inplace_object = backfill_inplace["object"] +backfill_inplace_int64 = backfill_inplace["int64_t"] +backfill_inplace_int32 = backfill_inplace["int32_t"] +backfill_inplace_uint64 = backfill_inplace["uint64_t"] +backfill_inplace_bool = backfill_inplace["uint8_t"] + + +@cython.boundscheck(False) +@cython.wraparound(False) +def backfill_2d_inplace(ndarray[algos_t, ndim=2] values, + ndarray[uint8_t, ndim=2] mask, + limit=None): + cdef: + Py_ssize_t i, j, N, K + algos_t val + int lim, fill_count = 0 + + K, N = ( values).shape + + # GH#2778 + if N == 0: + return + + if limit is None: + lim = N + else: + if not util.is_integer_object(limit): + raise ValueError('Limit must be an integer') + if limit < 1: + raise ValueError('Limit must be greater than 0') + lim = limit + + for j in range(K): + fill_count = 0 + val = values[j, N - 1] + for i in range(N - 1, -1, -1): + if mask[j, i]: + if fill_count >= lim: + continue + fill_count += 1 + values[j, i] = val + else: + fill_count = 0 + val = values[j, i] + + +backfill_2d_inplace_float64 = backfill_2d_inplace["float64_t"] +backfill_2d_inplace_float32 = backfill_2d_inplace["float32_t"] +backfill_2d_inplace_object = backfill_2d_inplace["object"] +backfill_2d_inplace_int64 = backfill_2d_inplace["int64_t"] +backfill_2d_inplace_int32 = backfill_2d_inplace["int32_t"] +backfill_2d_inplace_uint64 = backfill_2d_inplace["uint64_t"] +backfill_2d_inplace_bool = backfill_2d_inplace["uint8_t"] + + +@cython.wraparound(False) +@cython.boundscheck(False) +def arrmap(ndarray[algos_t] index, object func): + cdef: + Py_ssize_t length = index.shape[0] + Py_ssize_t i = 0 + ndarray[object] result = np.empty(length, dtype=np.object_) + + from pandas._libs.lib import maybe_convert_objects + + for i in range(length): + result[i] = func(index[i]) + + return maybe_convert_objects(result) + + +arrmap_float64 = arrmap["float64_t"] +arrmap_float32 = arrmap["float32_t"] +arrmap_object = arrmap["object"] +arrmap_int64 = arrmap["int64_t"] +arrmap_int32 = arrmap["int32_t"] +arrmap_uint64 = arrmap["uint64_t"] +arrmap_bool = arrmap["uint8_t"] + + +@cython.boundscheck(False) +@cython.wraparound(False) +def is_monotonic(ndarray[algos_t] arr, bint timelike): + """ + Returns + ------- + is_monotonic_inc, is_monotonic_dec, is_unique + """ + cdef: + Py_ssize_t i, n + algos_t prev, cur + bint is_monotonic_inc = 1 + bint is_monotonic_dec = 1 + bint is_unique = 1 + bint is_strict_monotonic = 1 + + n = len(arr) + + if n == 1: + if arr[0] != arr[0] or (timelike and arr[0] == iNaT): + # single value is NaN + return False, False, True + else: + return True, True, True + elif n < 2: + return True, True, True + + if timelike and arr[0] == iNaT: + return False, False, True + + if algos_t is not object: + with nogil: + prev = arr[0] + for i in range(1, n): + cur = arr[i] + if timelike and cur == iNaT: + is_monotonic_inc = 0 + is_monotonic_dec = 0 + break + if cur < prev: + is_monotonic_inc = 0 + elif cur > prev: + is_monotonic_dec = 0 + elif cur == prev: + is_unique = 0 + else: + # cur or prev is NaN + is_monotonic_inc = 0 + is_monotonic_dec = 0 + break + if not is_monotonic_inc and not is_monotonic_dec: + is_monotonic_inc = 0 + is_monotonic_dec = 0 + break + prev = cur + else: + # object-dtype, identical to above except we cannot use `with nogil` + prev = arr[0] + for i in range(1, n): + cur = arr[i] + if timelike and cur == iNaT: + is_monotonic_inc = 0 + is_monotonic_dec = 0 + break + if cur < prev: + is_monotonic_inc = 0 + elif cur > prev: + is_monotonic_dec = 0 + elif cur == prev: + is_unique = 0 + else: + # cur or prev is NaN + is_monotonic_inc = 0 + is_monotonic_dec = 0 + break + if not is_monotonic_inc and not is_monotonic_dec: + is_monotonic_inc = 0 + is_monotonic_dec = 0 + break + prev = cur + + is_strict_monotonic = is_unique and (is_monotonic_inc or is_monotonic_dec) + return is_monotonic_inc, is_monotonic_dec, is_strict_monotonic + + +is_monotonic_float64 = is_monotonic["float64_t"] +is_monotonic_float32 = is_monotonic["float32_t"] +is_monotonic_object = is_monotonic["object"] +is_monotonic_int64 = is_monotonic["int64_t"] +is_monotonic_int32 = is_monotonic["int32_t"] +is_monotonic_uint64 = is_monotonic["uint64_t"] +is_monotonic_bool = is_monotonic["uint8_t"] + + # generated from template include "algos_common_helper.pxi" include "algos_rank_helper.pxi" diff --git a/pandas/_libs/algos_common_helper.pxi.in b/pandas/_libs/algos_common_helper.pxi.in index ed4c0e4c59609..40b1b1a282670 100644 --- a/pandas/_libs/algos_common_helper.pxi.in +++ b/pandas/_libs/algos_common_helper.pxi.in @@ -15,443 +15,6 @@ Template for each `dtype` helper function using 1-d template WARNING: DO NOT edit .pxi FILE directly, .pxi is generated from .pxi.in """ -#---------------------------------------------------------------------- -# 1-d template -#---------------------------------------------------------------------- - -{{py: - -# name, c_type, dtype, can_hold_na, nogil -dtypes = [('float64', 'float64_t', 'np.float64', True, True), - ('float32', 'float32_t', 'np.float32', True, True), - ('object', 'object', 'object', True, False), - ('int32', 'int32_t', 'np.int32', False, True), - ('int64', 'int64_t', 'np.int64', False, True), - ('uint64', 'uint64_t', 'np.uint64', False, True), - ('bool', 'uint8_t', 'np.bool', False, True)] - -def get_dispatch(dtypes): - - for name, c_type, dtype, can_hold_na, nogil in dtypes: - - nogil_str = 'with nogil:' if nogil else '' - tab = ' ' if nogil else '' - yield name, c_type, dtype, can_hold_na, nogil_str, tab -}} - -{{for name, c_type, dtype, can_hold_na, nogil_str, tab - in get_dispatch(dtypes)}} - - -@cython.wraparound(False) -@cython.boundscheck(False) -def map_indices_{{name}}(ndarray[{{c_type}}] index): - """ - Produce a dict mapping the values of the input array to their respective - locations. - - Example: - array(['hi', 'there']) --> {'hi' : 0 , 'there' : 1} - - Better to do this with Cython because of the enormous speed boost. - """ - cdef: - Py_ssize_t i, length - dict result = {} - - length = len(index) - - for i in range(length): - result[index[i]] = i - - return result - - -@cython.boundscheck(False) -@cython.wraparound(False) -def pad_{{name}}(ndarray[{{c_type}}] old, ndarray[{{c_type}}] new, limit=None): - cdef: - Py_ssize_t i, j, nleft, nright - ndarray[int64_t, ndim=1] indexer - {{c_type}} cur, next - int lim, fill_count = 0 - - nleft = len(old) - nright = len(new) - indexer = np.empty(nright, dtype=np.int64) - indexer.fill(-1) - - if limit is None: - lim = nright - else: - if not util.is_integer_object(limit): - raise ValueError('Limit must be an integer') - if limit < 1: - raise ValueError('Limit must be greater than 0') - lim = limit - - if nleft == 0 or nright == 0 or new[nright - 1] < old[0]: - return indexer - - i = j = 0 - - cur = old[0] - - while j <= nright - 1 and new[j] < cur: - j += 1 - - while True: - if j == nright: - break - - if i == nleft - 1: - while j < nright: - if new[j] == cur: - indexer[j] = i - elif new[j] > cur and fill_count < lim: - indexer[j] = i - fill_count += 1 - j += 1 - break - - next = old[i + 1] - - while j < nright and cur <= new[j] < next: - if new[j] == cur: - indexer[j] = i - elif fill_count < lim: - indexer[j] = i - fill_count += 1 - j += 1 - - fill_count = 0 - i += 1 - cur = next - - return indexer - - -@cython.boundscheck(False) -@cython.wraparound(False) -def pad_inplace_{{name}}(ndarray[{{c_type}}] values, - ndarray[uint8_t, cast=True] mask, - limit=None): - cdef: - Py_ssize_t i, N - {{c_type}} val - int lim, fill_count = 0 - - N = len(values) - - # GH 2778 - if N == 0: - return - - if limit is None: - lim = N - else: - if not util.is_integer_object(limit): - raise ValueError('Limit must be an integer') - if limit < 1: - raise ValueError('Limit must be greater than 0') - lim = limit - - val = values[0] - for i in range(N): - if mask[i]: - if fill_count >= lim: - continue - fill_count += 1 - values[i] = val - else: - fill_count = 0 - val = values[i] - - -@cython.boundscheck(False) -@cython.wraparound(False) -def pad_2d_inplace_{{name}}(ndarray[{{c_type}}, ndim=2] values, - ndarray[uint8_t, ndim=2] mask, - limit=None): - cdef: - Py_ssize_t i, j, N, K - {{c_type}} val - int lim, fill_count = 0 - - K, N = ( values).shape - - # GH 2778 - if N == 0: - return - - if limit is None: - lim = N - else: - if not util.is_integer_object(limit): - raise ValueError('Limit must be an integer') - if limit < 1: - raise ValueError('Limit must be greater than 0') - lim = limit - - for j in range(K): - fill_count = 0 - val = values[j, 0] - for i in range(N): - if mask[j, i]: - if fill_count >= lim: - continue - fill_count += 1 - values[j, i] = val - else: - fill_count = 0 - val = values[j, i] - -""" -Backfilling logic for generating fill vector - -Diagram of what's going on - -Old New Fill vector Mask - . 0 1 - . 0 1 - . 0 1 -A A 0 1 - . 1 1 - . 1 1 - . 1 1 - . 1 1 - . 1 1 -B B 1 1 - . 2 1 - . 2 1 - . 2 1 -C C 2 1 - . 0 - . 0 -D -""" - - -@cython.boundscheck(False) -@cython.wraparound(False) -def backfill_{{name}}(ndarray[{{c_type}}] old, ndarray[{{c_type}}] new, - limit=None): - cdef: - Py_ssize_t i, j, nleft, nright - ndarray[int64_t, ndim=1] indexer - {{c_type}} cur, prev - int lim, fill_count = 0 - - nleft = len(old) - nright = len(new) - indexer = np.empty(nright, dtype=np.int64) - indexer.fill(-1) - - if limit is None: - lim = nright - else: - if not util.is_integer_object(limit): - raise ValueError('Limit must be an integer') - if limit < 1: - raise ValueError('Limit must be greater than 0') - lim = limit - - if nleft == 0 or nright == 0 or new[0] > old[nleft - 1]: - return indexer - - i = nleft - 1 - j = nright - 1 - - cur = old[nleft - 1] - - while j >= 0 and new[j] > cur: - j -= 1 - - while True: - if j < 0: - break - - if i == 0: - while j >= 0: - if new[j] == cur: - indexer[j] = i - elif new[j] < cur and fill_count < lim: - indexer[j] = i - fill_count += 1 - j -= 1 - break - - prev = old[i - 1] - - while j >= 0 and prev < new[j] <= cur: - if new[j] == cur: - indexer[j] = i - elif new[j] < cur and fill_count < lim: - indexer[j] = i - fill_count += 1 - j -= 1 - - fill_count = 0 - i -= 1 - cur = prev - - return indexer - - -@cython.boundscheck(False) -@cython.wraparound(False) -def backfill_inplace_{{name}}(ndarray[{{c_type}}] values, - ndarray[uint8_t, cast=True] mask, - limit=None): - cdef: - Py_ssize_t i, N - {{c_type}} val - int lim, fill_count = 0 - - N = len(values) - - # GH 2778 - if N == 0: - return - - if limit is None: - lim = N - else: - if not util.is_integer_object(limit): - raise ValueError('Limit must be an integer') - if limit < 1: - raise ValueError('Limit must be greater than 0') - lim = limit - - val = values[N - 1] - for i in range(N - 1, -1, -1): - if mask[i]: - if fill_count >= lim: - continue - fill_count += 1 - values[i] = val - else: - fill_count = 0 - val = values[i] - - -@cython.boundscheck(False) -@cython.wraparound(False) -def backfill_2d_inplace_{{name}}(ndarray[{{c_type}}, ndim=2] values, - ndarray[uint8_t, ndim=2] mask, - limit=None): - cdef: - Py_ssize_t i, j, N, K - {{c_type}} val - int lim, fill_count = 0 - - K, N = ( values).shape - - # GH 2778 - if N == 0: - return - - if limit is None: - lim = N - else: - if not util.is_integer_object(limit): - raise ValueError('Limit must be an integer') - if limit < 1: - raise ValueError('Limit must be greater than 0') - lim = limit - - for j in range(K): - fill_count = 0 - val = values[j, N - 1] - for i in range(N - 1, -1, -1): - if mask[j, i]: - if fill_count >= lim: - continue - fill_count += 1 - values[j, i] = val - else: - fill_count = 0 - val = values[j, i] - - -@cython.boundscheck(False) -@cython.wraparound(False) -def is_monotonic_{{name}}(ndarray[{{c_type}}] arr, bint timelike): - """ - Returns - ------- - is_monotonic_inc, is_monotonic_dec, is_unique - """ - cdef: - Py_ssize_t i, n - {{c_type}} prev, cur - bint is_monotonic_inc = 1 - bint is_monotonic_dec = 1 - bint is_unique = 1 - - n = len(arr) - - if n == 1: - if arr[0] != arr[0] or (timelike and arr[0] == iNaT): - # single value is NaN - return False, False, True - else: - return True, True, True - elif n < 2: - return True, True, True - - if timelike and arr[0] == iNaT: - return False, False, True - - {{nogil_str}} - {{tab}}prev = arr[0] - {{tab}}for i in range(1, n): - {{tab}} cur = arr[i] - {{tab}} if timelike and cur == iNaT: - {{tab}} is_monotonic_inc = 0 - {{tab}} is_monotonic_dec = 0 - {{tab}} break - {{tab}} if cur < prev: - {{tab}} is_monotonic_inc = 0 - {{tab}} elif cur > prev: - {{tab}} is_monotonic_dec = 0 - {{tab}} elif cur == prev: - {{tab}} is_unique = 0 - {{tab}} else: - {{tab}} # cur or prev is NaN - {{tab}} is_monotonic_inc = 0 - {{tab}} is_monotonic_dec = 0 - {{tab}} break - {{tab}} if not is_monotonic_inc and not is_monotonic_dec: - {{tab}} is_monotonic_inc = 0 - {{tab}} is_monotonic_dec = 0 - {{tab}} break - {{tab}} prev = cur - return is_monotonic_inc, is_monotonic_dec, \ - is_unique and (is_monotonic_inc or is_monotonic_dec) - - -@cython.wraparound(False) -@cython.boundscheck(False) -def arrmap_{{name}}(ndarray[{{c_type}}] index, object func): - cdef: - Py_ssize_t length = index.shape[0] - Py_ssize_t i = 0 - ndarray[object] result = np.empty(length, dtype=np.object_) - - from pandas._libs.lib import maybe_convert_objects - - for i in range(length): - result[i] = func(index[i]) - - return maybe_convert_objects(result) - -{{endfor}} - -#---------------------------------------------------------------------- -# put template -#---------------------------------------------------------------------- - {{py: # name, c_type, dest_type, dest_dtype From c994e803820d2e6d1fc7a3c2143c98d351b6f3bd Mon Sep 17 00:00:00 2001 From: Luca Donini Date: Tue, 18 Sep 2018 13:46:36 +0100 Subject: [PATCH 14/87] DOC: Updating the docstring of Series.str.extractall (#22565) --- pandas/core/strings.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index b46c6a4557ff3..ed091ce4956bc 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -957,19 +957,23 @@ def str_extractall(arr, pat, flags=0): Parameters ---------- - pat : string - Regular expression pattern with capturing groups + pat : str + Regular expression pattern with capturing groups. flags : int, default 0 (no flags) - re module flags, e.g. re.IGNORECASE + A ``re`` module flag, for example ``re.IGNORECASE``. These allow + to modify regular expression matching for things like case, spaces, + etc. Multiple flags can be combined with the bitwise OR operator, + for example ``re.IGNORECASE | re.MULTILINE``. Returns ------- - A DataFrame with one row for each match, and one column for each - group. Its rows have a MultiIndex with first levels that come from - the subject Series. The last level is named 'match' and indicates - the order in the subject. Any capture group names in regular - expression pat will be used for column names; otherwise capture - group numbers will be used. + DataFrame + A ``DataFrame`` with one row for each match, and one column for each + group. Its rows have a ``MultiIndex`` with first levels that come from + the subject ``Series``. The last level is named 'match' and indexes the + matches in each item of the ``Series``. Any capture group names in + regular expression pat will be used for column names; otherwise capture + group numbers will be used. See Also -------- @@ -1015,7 +1019,6 @@ def str_extractall(arr, pat, flags=0): 1 a 2 B 0 b 1 C 0 NaN 1 - """ regex = re.compile(pat, flags=flags) From d39249ad8ffc201dc7e92b063e2c8c2823b1cdc7 Mon Sep 17 00:00:00 2001 From: realead Date: Tue, 18 Sep 2018 14:51:15 +0200 Subject: [PATCH 15/87] BUG: don't mangle NaN-float-values and pd.NaT (GH 22295) (#22296) --- doc/source/whatsnew/v0.24.0.txt | 5 ++- pandas/_libs/hashtable_class_helper.pxi.in | 52 +++------------------- pandas/conftest.py | 12 +++++ pandas/core/indexes/base.py | 5 --- pandas/core/indexes/numeric.py | 8 ++++ pandas/tests/indexes/test_base.py | 20 ++++++++- pandas/tests/test_algos.py | 30 +++++++++++++ 7 files changed, 79 insertions(+), 53 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 1dd8bd401face..30745f186edcc 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -721,13 +721,16 @@ Indexing - Bug where mixed indexes wouldn't allow integers for ``.at`` (:issue:`19860`) - ``Float64Index.get_loc`` now raises ``KeyError`` when boolean key passed. (:issue:`19087`) - Bug in :meth:`DataFrame.loc` when indexing with an :class:`IntervalIndex` (:issue:`19977`) +- :class:`Index` no longer mangles ``None``, ``NaN`` and ``NaT``, i.e. they are treated as three different keys. However, for numeric Index all three are still coerced to a ``NaN`` (:issue:`22332`) Missing ^^^^^^^ - Bug in :func:`DataFrame.fillna` where a ``ValueError`` would raise when one column contained a ``datetime64[ns, tz]`` dtype (:issue:`15522`) - Bug in :func:`Series.hasnans` that could be incorrectly cached and return incorrect answers if null elements are introduced after an initial call (:issue:`19700`) -- :func:`Series.isin` now treats all nans as equal also for ``np.object``-dtype. This behavior is consistent with the behavior for float64 (:issue:`22119`) +- :func:`Series.isin` now treats all NaN-floats as equal also for `np.object`-dtype. This behavior is consistent with the behavior for float64 (:issue:`22119`) +- :func:`unique` no longer mangles NaN-floats and the ``NaT``-object for `np.object`-dtype, i.e. ``NaT`` is no longer coerced to a NaN-value and is treated as a different entity. (:issue:`22295`) + MultiIndex ^^^^^^^^^^ diff --git a/pandas/_libs/hashtable_class_helper.pxi.in b/pandas/_libs/hashtable_class_helper.pxi.in index 550cabd5e3192..f294fd141a9f1 100644 --- a/pandas/_libs/hashtable_class_helper.pxi.in +++ b/pandas/_libs/hashtable_class_helper.pxi.in @@ -470,7 +470,6 @@ cdef class {{name}}HashTable(HashTable): int ret = 0 {{dtype}}_t val khiter_t k - bint seen_na = 0 {{name}}Vector uniques = {{name}}Vector() {{name}}VectorData *ud @@ -479,22 +478,6 @@ cdef class {{name}}HashTable(HashTable): with nogil: for i in range(n): val = values[i] - {{if float_group}} - if val == val: - k = kh_get_{{dtype}}(self.table, val) - if k == self.table.n_buckets: - kh_put_{{dtype}}(self.table, val, &ret) - if needs_resize(ud): - with gil: - uniques.resize() - append_data_{{dtype}}(ud, val) - elif not seen_na: - seen_na = 1 - if needs_resize(ud): - with gil: - uniques.resize() - append_data_{{dtype}}(ud, NAN) - {{else}} k = kh_get_{{dtype}}(self.table, val) if k == self.table.n_buckets: kh_put_{{dtype}}(self.table, val, &ret) @@ -502,7 +485,6 @@ cdef class {{name}}HashTable(HashTable): with gil: uniques.resize() append_data_{{dtype}}(ud, val) - {{endif}} return uniques.to_array() {{endfor}} @@ -747,9 +729,6 @@ cdef class StringHashTable(HashTable): return np.asarray(labels) -na_sentinel = object - - cdef class PyObjectHashTable(HashTable): def __init__(self, size_hint=1): @@ -767,8 +746,7 @@ cdef class PyObjectHashTable(HashTable): def __contains__(self, object key): cdef khiter_t k hash(key) - if key != key or key is None: - key = na_sentinel + k = kh_get_pymap(self.table, key) return k != self.table.n_buckets @@ -780,8 +758,7 @@ cdef class PyObjectHashTable(HashTable): cpdef get_item(self, object val): cdef khiter_t k - if val != val or val is None: - val = na_sentinel + k = kh_get_pymap(self.table, val) if k != self.table.n_buckets: return self.table.vals[k] @@ -795,8 +772,7 @@ cdef class PyObjectHashTable(HashTable): char* buf hash(key) - if key != key or key is None: - key = na_sentinel + k = kh_put_pymap(self.table, key, &ret) # self.table.keys[k] = key if kh_exist_pymap(self.table, k): @@ -814,8 +790,6 @@ cdef class PyObjectHashTable(HashTable): for i in range(n): val = values[i] hash(val) - if val != val or val is None: - val = na_sentinel k = kh_put_pymap(self.table, val, &ret) self.table.vals[k] = i @@ -831,8 +805,6 @@ cdef class PyObjectHashTable(HashTable): for i in range(n): val = values[i] hash(val) - if val != val or val is None: - val = na_sentinel k = kh_get_pymap(self.table, val) if k != self.table.n_buckets: @@ -849,24 +821,14 @@ cdef class PyObjectHashTable(HashTable): object val khiter_t k ObjectVector uniques = ObjectVector() - bint seen_na = 0 for i in range(n): val = values[i] hash(val) - - # `val is None` below is exception to prevent mangling of None and - # other NA values; note however that other NA values (ex: pd.NaT - # and np.nan) will still get mangled, so many not be a permanent - # solution; see GH 20866 - if not checknull(val) or val is None: - k = kh_get_pymap(self.table, val) - if k == self.table.n_buckets: - kh_put_pymap(self.table, val, &ret) - uniques.append(val) - elif not seen_na: - seen_na = 1 - uniques.append(nan) + k = kh_get_pymap(self.table, val) + if k == self.table.n_buckets: + kh_put_pymap(self.table, val, &ret) + uniques.append(val) return uniques.to_array() diff --git a/pandas/conftest.py b/pandas/conftest.py index 28c24fc8c0640..621de3ffd4b12 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -286,6 +286,18 @@ def nulls_fixture(request): nulls_fixture2 = nulls_fixture # Generate cartesian product of nulls_fixture +@pytest.fixture(params=[None, np.nan, pd.NaT]) +def unique_nulls_fixture(request): + """ + Fixture for each null type in pandas, each null type exactly once + """ + return request.param + + +# Generate cartesian product of unique_nulls_fixture: +unique_nulls_fixture2 = unique_nulls_fixture + + TIMEZONES = [None, 'UTC', 'US/Eastern', 'Asia/Tokyo', 'dateutil/US/Pacific', 'dateutil/Asia/Singapore'] diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index ca381160de0df..487d3975a6219 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3109,7 +3109,6 @@ def get_loc(self, key, method=None, tolerance=None): return self._engine.get_loc(key) except KeyError: return self._engine.get_loc(self._maybe_cast_indexer(key)) - indexer = self.get_indexer([key], method=method, tolerance=tolerance) if indexer.ndim > 1 or indexer.size > 1: raise TypeError('get_loc requires scalar valued input') @@ -4475,10 +4474,6 @@ def insert(self, loc, item): ------- new_index : Index """ - if is_scalar(item) and isna(item): - # GH 18295 - item = self._na_value - _self = np.asarray(self) item = self._coerce_scalar_to_index(item)._ndarray_values idx = np.concatenate((_self[:loc], item, _self[loc:])) diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index e0627432cbc2e..8d616468a87d9 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -9,6 +9,7 @@ is_bool, is_bool_dtype, is_scalar) +from pandas.core.dtypes.missing import isna from pandas import compat from pandas.core import algorithms @@ -114,6 +115,13 @@ def is_all_dates(self): """ return False + @Appender(Index.insert.__doc__) + def insert(self, loc, item): + # treat NA values as nans: + if is_scalar(item) and isna(item): + item = self._na_value + return super(NumericIndex, self).insert(loc, item) + _num_index_shared_docs['class_descr'] = """ Immutable ndarray implementing an ordered, sliceable set. The basic object diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 755b3cc7f1dca..eab04419fe939 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -560,8 +560,9 @@ def test_insert(self): tm.assert_index_equal(Index(['a']), null_index.insert(0, 'a')) def test_insert_missing(self, nulls_fixture): - # GH 18295 (test missing) - expected = Index(['a', np.nan, 'b', 'c']) + # GH 22295 + # test there is no mangling of NA values + expected = Index(['a', nulls_fixture, 'b', 'c']) result = Index(list('abc')).insert(1, nulls_fixture) tm.assert_index_equal(result, expected) @@ -1364,6 +1365,21 @@ def test_get_indexer_numeric_index_boolean_target(self): expected = np.array([-1, -1, -1], dtype=np.intp) tm.assert_numpy_array_equal(result, expected) + def test_get_indexer_with_NA_values(self, unique_nulls_fixture, + unique_nulls_fixture2): + # GH 22332 + # check pairwise, that no pair of na values + # is mangled + if unique_nulls_fixture is unique_nulls_fixture2: + return # skip it, values are not unique + arr = np.array([unique_nulls_fixture, + unique_nulls_fixture2], dtype=np.object) + index = pd.Index(arr, dtype=np.object) + result = index.get_indexer([unique_nulls_fixture, + unique_nulls_fixture2, 'Unknown']) + expected = np.array([0, 1, -1], dtype=np.int64) + tm.assert_numpy_array_equal(result, expected) + @pytest.mark.parametrize("method", [None, 'pad', 'backfill', 'nearest']) def test_get_loc(self, method): index = pd.Index([0, 1, 2]) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 64d2e155aa9a9..b2ddbf715b480 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -520,6 +520,36 @@ def test_different_nans(self): 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): From c44581f33f3569c25716285b8e09b234c31671da Mon Sep 17 00:00:00 2001 From: "SEUNG HOON, SHIN" Date: Tue, 18 Sep 2018 21:54:31 +0900 Subject: [PATCH 16/87] DOC: Expose ExcelWriter as part of the Generated API (#22359) --- doc/source/api.rst | 6 ++++++ pandas/core/generic.py | 14 ++++++++++---- pandas/io/excel.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index 9c3770a497cf8..e4b055c14ec27 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -61,6 +61,12 @@ Excel read_excel ExcelFile.parse +.. autosummary:: + :toctree: generated/ + :template: autosummary/class_without_autosummary.rst + + ExcelWriter + JSON ~~~~ diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 96a956764ce06..373830ec7892e 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1979,11 +1979,17 @@ def _repr_latex_(self): If you wish to write to more than one sheet in the workbook, it is necessary to specify an ExcelWriter object: - >>> writer = pd.ExcelWriter('output2.xlsx', engine='xlsxwriter') - >>> df1.to_excel(writer, sheet_name='Sheet1') >>> df2 = df1.copy() - >>> df2.to_excel(writer, sheet_name='Sheet2') - >>> writer.save() + >>> with pd.ExcelWriter('output.xlsx') as writer: + ... df1.to_excel(writer, sheet_name='Sheet_name_1') + ... df2.to_excel(writer, sheet_name='Sheet_name_2') + + To set the library that is used to write the Excel file, + you can pass the `engine` keyword (the default engine is + automatically chosen depending on the file extension): + + >>> df1.to_excel('output1.xlsx', engine='xlsxwriter') + """ def to_json(self, path_or_buf=None, orient=None, date_format=None, diff --git a/pandas/io/excel.py b/pandas/io/excel.py index e2db6643c5ef0..00b4c704c681b 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -824,8 +824,43 @@ class ExcelWriter(object): Notes ----- + None of the methods and properties are considered public. + For compatibility with CSV writers, ExcelWriter serializes lists and dicts to strings before writing. + + Examples + -------- + Default usage: + + >>> with ExcelWriter('path_to_file.xlsx') as writer: + ... df.to_excel(writer) + + To write to separate sheets in a single file: + + >>> with ExcelWriter('path_to_file.xlsx') as writer: + ... df1.to_excel(writer, sheet_name='Sheet1') + ... df2.to_excel(writer, sheet_name='Sheet2') + + You can set the date format or datetime format: + + >>> with ExcelWriter('path_to_file.xlsx', + date_format='YYYY-MM-DD', + datetime_format='YYYY-MM-DD HH:MM:SS') as writer: + ... df.to_excel(writer) + + You can also append to an existing Excel file: + + >>> with ExcelWriter('path_to_file.xlsx', mode='a') as writer: + ... df.to_excel(writer, sheet_name='Sheet3') + + Attributes + ---------- + None + + Methods + ------- + None """ # Defining an ExcelWriter implementation (see abstract methods for more...) From ace341efcaabddd3d694aca11bf734a5d4b1f31d Mon Sep 17 00:00:00 2001 From: Thierry Moisan Date: Tue, 18 Sep 2018 09:00:17 -0400 Subject: [PATCH 17/87] Test in scripts/validate_docstrings.py that the short summary is always one line long (#22617) --- scripts/tests/test_validate_docstrings.py | 13 ++++++++++++- scripts/validate_docstrings.py | 8 ++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/tests/test_validate_docstrings.py b/scripts/tests/test_validate_docstrings.py index 0c0757c6963d7..00496f771570b 100644 --- a/scripts/tests/test_validate_docstrings.py +++ b/scripts/tests/test_validate_docstrings.py @@ -362,6 +362,15 @@ def multi_line(self): 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): """ @@ -556,7 +565,9 @@ def test_bad_generic_functions(self, func): ('BadSummaries', 'no_capitalization', ('Summary must start with infinitive verb',)), ('BadSummaries', 'multi_line', - ('a short summary in a single line should be present',)), + ('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',)), diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 83bb382480eaa..790a62b53845b 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -163,10 +163,12 @@ def double_blank_lines(self): @property def summary(self): - if not self.doc['Extended Summary'] and len(self.doc['Summary']) > 1: - return '' 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: @@ -452,6 +454,8 @@ def validate_one(func_name): errs.append('Summary must start with infinitive verb, ' 'not third person (e.g. use "Generate" instead of ' '"Generates")') + if doc.num_summary_lines > 1: + errs.append("Summary should fit in a single line.") if not doc.extended_summary: wrns.append('No extended summary found') From dbb767cd9bdf96c2d7c9fe9b8dd5f564c83f3e47 Mon Sep 17 00:00:00 2001 From: Ben Nelson Date: Tue, 18 Sep 2018 06:25:25 -0700 Subject: [PATCH 18/87] fix raise of TypeError when subtracting timedelta array (#22054) closes #21980 --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/_libs/tslibs/timedeltas.pyx | 8 ++- .../tests/scalar/timedelta/test_arithmetic.py | 65 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 30745f186edcc..1b8e5757a15fd 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -646,6 +646,7 @@ Timedelta - Bug in :class:`Series` with numeric dtype when adding or subtracting an an array or ``Series`` with ``timedelta64`` dtype (:issue:`22390`) - Bug in :class:`Index` with numeric dtype when multiplying or dividing an array with dtype ``timedelta64`` (:issue:`22390`) - Bug in :class:`TimedeltaIndex` incorrectly allowing indexing with ``Timestamp`` object (:issue:`20464`) +- Fixed bug where subtracting :class:`Timedelta` from an object-dtyped array would raise ``TypeError`` (:issue:`21980`) - - diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 9b13ef5982396..9c8be1901d1dc 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -541,10 +541,12 @@ def _binary_op_method_timedeltalike(op, name): elif hasattr(other, 'dtype'): # nd-array like - if other.dtype.kind not in ['m', 'M']: - # raise rathering than letting numpy return wrong answer + if other.dtype.kind in ['m', 'M']: + return op(self.to_timedelta64(), other) + elif other.dtype.kind == 'O': + return np.array([op(self, x) for x in other]) + else: return NotImplemented - return op(self.to_timedelta64(), other) elif not _validate_ops_compat(other): return NotImplemented diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index 9636c92ec22d5..fce1ef29235cc 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -200,6 +200,57 @@ def test_td_rsub_numeric_raises(self): with pytest.raises(TypeError): 2.0 - td + 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 - pd.Timedelta('1D') + tm.assert_numpy_array_equal(res, exp) + + def test_td_sub_mixed_most_timedeltalike_object_dtype_array(self): + # GH 21980 + now = pd.Timestamp.now() + arr = np.array([now, + pd.Timedelta('1D'), + np.timedelta64(2, 'h')]) + exp = np.array([now - pd.Timedelta('1D'), + pd.Timedelta('0D'), + np.timedelta64(2, 'h') - pd.Timedelta('1D')]) + res = arr - pd.Timedelta('1D') + tm.assert_numpy_array_equal(res, exp) + + def test_td_rsub_mixed_most_timedeltalike_object_dtype_array(self): + # GH 21980 + now = pd.Timestamp.now() + arr = np.array([now, + pd.Timedelta('1D'), + np.timedelta64(2, 'h')]) + with pytest.raises(TypeError): + pd.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, pd.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 = pd.Timestamp.now() + arr = np.array([now, + pd.Timedelta('1D')]) + exp = np.array([now + pd.Timedelta('1D'), + pd.Timedelta('2D')]) + res = op(arr, pd.Timedelta('1D')) + tm.assert_numpy_array_equal(res, exp) + class TestTimedeltaMultiplicationDivision(object): """ @@ -616,3 +667,17 @@ def test_rdivmod_invalid(self): 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([pd.Timestamp.now(), pd.Timedelta('1D')]) + ]) + def test_td_op_timedelta_timedeltalike_array(self, op, arr): + with pytest.raises(TypeError): + op(arr, pd.Timedelta('1D')) From 4e0b636d2f11c7a077b46eca1b2ed3e59b682e7e Mon Sep 17 00:00:00 2001 From: Mak Sze Chun Date: Tue, 18 Sep 2018 21:54:14 +0800 Subject: [PATCH 19/87] Bug: Logical operator of Series with Index (#22092) (#22293) * Fix bug #GH22092 * Update v0.24.0.txt * Update v0.24.0.txt * Update ops.py * Update test_operators.py * Update v0.24.0.txt * Update test_operators.py --- doc/source/whatsnew/v0.24.0.txt | 3 ++- pandas/core/ops.py | 2 +- pandas/tests/series/test_operators.py | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 1b8e5757a15fd..39ed5d968707b 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -803,7 +803,8 @@ Other - :meth:`~pandas.io.formats.style.Styler.background_gradient` now takes a ``text_color_threshold`` parameter to automatically lighten the text color based on the luminance of the background color. This improves readability with dark background colors without the need to limit the background colormap range. (:issue:`21258`) - Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`) - :meth:`~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`) -- :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax``. ``NaN`` values are also handled properly. (:issue:`21548`, :issue:`21526`) +- :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly. +- Logical operations ``&, |, ^`` between :class:`Series` and :class:`Index` will no longer raise ``ValueError`` (:issue:`22092`) - - - diff --git a/pandas/core/ops.py b/pandas/core/ops.py index ca9c2528f0aef..a7fc2839ea101 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -1533,7 +1533,7 @@ def na_op(x, y): if isinstance(y, list): y = construct_1d_object_array_from_listlike(y) - if isinstance(y, (np.ndarray, ABCSeries)): + if isinstance(y, (np.ndarray, ABCSeries, ABCIndexClass)): if (is_bool_dtype(x.dtype) and is_bool_dtype(y.dtype)): result = op(x, y) # when would this be hit? else: diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 5e5e9c0895ccf..615f0c9247bd8 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -425,6 +425,30 @@ 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_logical_ops_with_index(self): + # GH22092 + ser = Series([True, True, False, False]) + idx1 = Index([True, False, True, False]) + idx2 = Index([1, 0, 1, 0]) + + expected = Series([True, False, False, False]) + result1 = ser & idx1 + assert_series_equal(result1, expected) + result2 = ser & idx2 + assert_series_equal(result2, expected) + + expected = Series([True, True, True, False]) + result1 = ser | idx1 + assert_series_equal(result1, expected) + result2 = ser | idx2 + assert_series_equal(result2, expected) + + expected = Series([False, True, True, False]) + result1 = ser ^ idx1 + assert_series_equal(result1, expected) + result2 = ser ^ idx2 + assert_series_equal(result2, expected) + def test_ne(self): ts = Series([3, 4, 5, 6, 7], [3, 4, 5, 6, 7], dtype=float) expected = [True, True, False, True, True] From fc25f7d2f97185d520a6752a01290463ac35e871 Mon Sep 17 00:00:00 2001 From: Thierry Moisan Date: Tue, 18 Sep 2018 09:58:22 -0400 Subject: [PATCH 20/87] DOC: Fix Series nsmallest and nlargest docstring/doctests (#22731) --- ci/doctests.sh | 2 +- pandas/core/series.py | 187 ++++++++++++++++++++++++++++++++---------- 2 files changed, 144 insertions(+), 45 deletions(-) diff --git a/ci/doctests.sh b/ci/doctests.sh index 654bd57107904..a941515fde4ae 100755 --- a/ci/doctests.sh +++ b/ci/doctests.sh @@ -28,7 +28,7 @@ if [ "$DOCTEST" ]; then fi pytest --doctest-modules -v pandas/core/series.py \ - -k"-nlargest -nonzero -nsmallest -reindex -searchsorted -to_dict" + -k"-nonzero -reindex -searchsorted -to_dict" if [ $? -ne "0" ]; then RET=1 diff --git a/pandas/core/series.py b/pandas/core/series.py index 0268b8e9c3149..8f69de973e7a3 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2741,17 +2741,20 @@ def nlargest(self, n=5, keep='first'): Parameters ---------- - n : int - Return this many descending sorted values - keep : {'first', 'last'}, default 'first' - Where there are duplicate values: - - ``first`` : take the first occurrence. - - ``last`` : take the last occurrence. + n : int, default 5 + Return this many descending sorted values. + keep : {'first', 'last', 'all'}, default 'first' + When there are duplicate values that cannot all fit in a + Series of `n` elements: + - ``first`` : take the first occurrences based on the index order + - ``last`` : take the last occurrences based on the index order + - ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. Returns ------- - top_n : Series - The n largest values in the Series, in sorted order + Series + The `n` largest values in the Series, sorted in decreasing order. Notes ----- @@ -2760,23 +2763,70 @@ def nlargest(self, n=5, keep='first'): See Also -------- - Series.nsmallest + Series.nsmallest: Get the `n` smallest elements. + Series.sort_values: Sort Series by values. + Series.head: Return the first `n` rows. Examples -------- - >>> s = pd.Series(np.random.randn(10**6)) - >>> s.nlargest(10) # only sorts up to the N requested - 219921 4.644710 - 82124 4.608745 - 421689 4.564644 - 425277 4.447014 - 718691 4.414137 - 43154 4.403520 - 283187 4.313922 - 595519 4.273635 - 503969 4.250236 - 121637 4.240952 - dtype: float64 + >>> countries_population = {"Italy": 59000000, "France": 65000000, + ... "Malta": 434000, "Maldives": 434000, + ... "Brunei": 434000, "Iceland": 337000, + ... "Nauru": 11300, "Tuvalu": 11300, + ... "Anguilla": 11300, "Monserat": 5200} + >>> s = pd.Series(countries_population) + >>> s + Italy 59000000 + France 65000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + Iceland 337000 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + Monserat 5200 + dtype: int64 + + The `n` largest elements where ``n=5`` by default. + + >>> s.nlargest() + France 65000000 + Italy 59000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + dtype: int64 + + The `n` largest elements where ``n=3``. Default `keep` value is 'first' + so Malta will be kept. + + >>> s.nlargest(3) + France 65000000 + Italy 59000000 + Malta 434000 + dtype: int64 + + The `n` largest elements where ``n=3`` and keeping the last duplicates. + Brunei will be kept since it is the last with value 434000 based on + the index order. + + >>> s.nlargest(3, keep='last') + France 65000000 + Italy 59000000 + Brunei 434000 + dtype: int64 + + The `n` largest elements where ``n=3`` with all duplicates kept. Note + that the returned Series has five elements due to the three duplicates. + + >>> s.nlargest(3, keep='all') + France 65000000 + Italy 59000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + dtype: int64 """ return algorithms.SelectNSeries(self, n=n, keep=keep).nlargest() @@ -2786,17 +2836,20 @@ def nsmallest(self, n=5, keep='first'): Parameters ---------- - n : int - Return this many ascending sorted values - keep : {'first', 'last'}, default 'first' - Where there are duplicate values: - - ``first`` : take the first occurrence. - - ``last`` : take the last occurrence. + n : int, default 5 + Return this many ascending sorted values. + keep : {'first', 'last', 'all'}, default 'first' + When there are duplicate values that cannot all fit in a + Series of `n` elements: + - ``first`` : take the first occurrences based on the index order + - ``last`` : take the last occurrences based on the index order + - ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. Returns ------- - bottom_n : Series - The n smallest values in the Series, in sorted order + Series + The `n` smallest values in the Series, sorted in increasing order. Notes ----- @@ -2805,23 +2858,69 @@ def nsmallest(self, n=5, keep='first'): See Also -------- - Series.nlargest + Series.nlargest: Get the `n` largest elements. + Series.sort_values: Sort Series by values. + Series.head: Return the first `n` rows. Examples -------- - >>> s = pd.Series(np.random.randn(10**6)) - >>> s.nsmallest(10) # only sorts up to the N requested - 288532 -4.954580 - 732345 -4.835960 - 64803 -4.812550 - 446457 -4.609998 - 501225 -4.483945 - 669476 -4.472935 - 973615 -4.401699 - 621279 -4.355126 - 773916 -4.347355 - 359919 -4.331927 - dtype: float64 + >>> countries_population = {"Italy": 59000000, "France": 65000000, + ... "Brunei": 434000, "Malta": 434000, + ... "Maldives": 434000, "Iceland": 337000, + ... "Nauru": 11300, "Tuvalu": 11300, + ... "Anguilla": 11300, "Monserat": 5200} + >>> s = pd.Series(countries_population) + >>> s + Italy 59000000 + France 65000000 + Brunei 434000 + Malta 434000 + Maldives 434000 + Iceland 337000 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + Monserat 5200 + dtype: int64 + + The `n` largest elements where ``n=5`` by default. + + >>> s.nsmallest() + Monserat 5200 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + Iceland 337000 + dtype: int64 + + The `n` smallest elements where ``n=3``. Default `keep` value is + 'first' so Nauru and Tuvalu will be kept. + + >>> s.nsmallest(3) + Monserat 5200 + Nauru 11300 + Tuvalu 11300 + dtype: int64 + + The `n` smallest elements where ``n=3`` and keeping the last + duplicates. Anguilla and Tuvalu will be kept since they are the last + with value 11300 based on the index order. + + >>> s.nsmallest(3, keep='last') + Monserat 5200 + Anguilla 11300 + Tuvalu 11300 + dtype: int64 + + The `n` smallest elements where ``n=3`` with all duplicates kept. Note + that the returned Series has four elements due to the three duplicates. + + >>> s.nsmallest(3, keep='all') + Monserat 5200 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + dtype: int64 """ return algorithms.SelectNSeries(self, n=n, keep=keep).nsmallest() From 2670494d7a016fd67430acb8cc343d5b16f2d3e4 Mon Sep 17 00:00:00 2001 From: h-vetinari <33685575+h-vetinari@users.noreply.github.com> Date: Tue, 18 Sep 2018 16:33:55 +0200 Subject: [PATCH 21/87] Fixturize tests/frame/test_api and tests/sparse/frame/test_frame (#22738) --- pandas/tests/frame/test_api.py | 183 +++++---- pandas/tests/sparse/frame/conftest.py | 116 ++++++ pandas/tests/sparse/frame/test_frame.py | 477 ++++++++++++------------ 3 files changed, 439 insertions(+), 337 deletions(-) create mode 100644 pandas/tests/sparse/frame/conftest.py diff --git a/pandas/tests/frame/test_api.py b/pandas/tests/frame/test_api.py index 78a19029db567..35f2f566ef85e 100644 --- a/pandas/tests/frame/test_api.py +++ b/pandas/tests/frame/test_api.py @@ -24,8 +24,6 @@ import pandas.util.testing as tm -from pandas.tests.frame.common import TestData - class SharedWithSparse(object): """ @@ -43,57 +41,57 @@ def _assert_series_equal(self, left, right): """Dispatch to series class dependent assertion""" raise NotImplementedError - def test_copy_index_name_checking(self): + def test_copy_index_name_checking(self, float_frame): # don't want to be able to modify the index stored elsewhere after # making a copy for attr in ('index', 'columns'): - ind = getattr(self.frame, attr) + ind = getattr(float_frame, attr) ind.name = None - cp = self.frame.copy() + cp = float_frame.copy() getattr(cp, attr).name = 'foo' - assert getattr(self.frame, attr).name is None + assert getattr(float_frame, attr).name is None - def test_getitem_pop_assign_name(self): - s = self.frame['A'] + def test_getitem_pop_assign_name(self, float_frame): + s = float_frame['A'] assert s.name == 'A' - s = self.frame.pop('A') + s = float_frame.pop('A') assert s.name == 'A' - s = self.frame.loc[:, 'B'] + s = float_frame.loc[:, 'B'] assert s.name == 'B' s2 = s.loc[:] assert s2.name == 'B' - def test_get_value(self): - for idx in self.frame.index: - for col in self.frame.columns: + def test_get_value(self, float_frame): + for idx in float_frame.index: + for col in float_frame.columns: with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = self.frame.get_value(idx, col) - expected = self.frame[col][idx] + result = float_frame.get_value(idx, col) + expected = float_frame[col][idx] tm.assert_almost_equal(result, expected) - def test_add_prefix_suffix(self): - with_prefix = self.frame.add_prefix('foo#') - expected = pd.Index(['foo#%s' % c for c in self.frame.columns]) + def test_add_prefix_suffix(self, float_frame): + with_prefix = float_frame.add_prefix('foo#') + expected = pd.Index(['foo#%s' % c for c in float_frame.columns]) tm.assert_index_equal(with_prefix.columns, expected) - with_suffix = self.frame.add_suffix('#foo') - expected = pd.Index(['%s#foo' % c for c in self.frame.columns]) + with_suffix = float_frame.add_suffix('#foo') + expected = pd.Index(['%s#foo' % c for c in float_frame.columns]) tm.assert_index_equal(with_suffix.columns, expected) - with_pct_prefix = self.frame.add_prefix('%') - expected = pd.Index(['%{}'.format(c) for c in self.frame.columns]) + with_pct_prefix = float_frame.add_prefix('%') + expected = pd.Index(['%{}'.format(c) for c in float_frame.columns]) tm.assert_index_equal(with_pct_prefix.columns, expected) - with_pct_suffix = self.frame.add_suffix('%') - expected = pd.Index(['{}%'.format(c) for c in self.frame.columns]) + with_pct_suffix = float_frame.add_suffix('%') + expected = pd.Index(['{}%'.format(c) for c in float_frame.columns]) tm.assert_index_equal(with_pct_suffix.columns, expected) - def test_get_axis(self): - f = self.frame + def test_get_axis(self, float_frame): + f = float_frame assert f._get_axis_number(0) == 0 assert f._get_axis_number(1) == 1 assert f._get_axis_number('index') == 0 @@ -118,13 +116,13 @@ def test_get_axis(self): tm.assert_raises_regex(ValueError, 'No axis named', f._get_axis_number, None) - def test_keys(self): - getkeys = self.frame.keys - assert getkeys() is self.frame.columns + def test_keys(self, float_frame): + getkeys = float_frame.keys + assert getkeys() is float_frame.columns - def test_column_contains_typeerror(self): + def test_column_contains_typeerror(self, float_frame): try: - self.frame.columns in self.frame + float_frame.columns in float_frame except TypeError: pass @@ -146,10 +144,10 @@ def test_tab_completion(self): assert key not in dir(df) assert isinstance(df.__getitem__('A'), pd.DataFrame) - def test_not_hashable(self): + def test_not_hashable(self, empty_frame): df = self.klass([1]) pytest.raises(TypeError, hash, df) - pytest.raises(TypeError, hash, self.empty) + pytest.raises(TypeError, hash, empty_frame) def test_new_empty_index(self): df1 = self.klass(randn(0, 3)) @@ -157,29 +155,29 @@ def test_new_empty_index(self): df1.index.name = 'foo' assert df2.index.name is None - def test_array_interface(self): + def test_array_interface(self, float_frame): with np.errstate(all='ignore'): - result = np.sqrt(self.frame) - assert isinstance(result, type(self.frame)) - assert result.index is self.frame.index - assert result.columns is self.frame.columns + result = np.sqrt(float_frame) + assert isinstance(result, type(float_frame)) + assert result.index is float_frame.index + assert result.columns is float_frame.columns - self._assert_frame_equal(result, self.frame.apply(np.sqrt)) + self._assert_frame_equal(result, float_frame.apply(np.sqrt)) - def test_get_agg_axis(self): - cols = self.frame._get_agg_axis(0) - assert cols is self.frame.columns + def test_get_agg_axis(self, float_frame): + cols = float_frame._get_agg_axis(0) + assert cols is float_frame.columns - idx = self.frame._get_agg_axis(1) - assert idx is self.frame.index + idx = float_frame._get_agg_axis(1) + assert idx is float_frame.index - pytest.raises(ValueError, self.frame._get_agg_axis, 2) + pytest.raises(ValueError, float_frame._get_agg_axis, 2) - def test_nonzero(self): - assert self.empty.empty + def test_nonzero(self, float_frame, float_string_frame, empty_frame): + assert empty_frame.empty - assert not self.frame.empty - assert not self.mixed_frame.empty + assert not float_frame.empty + assert not float_string_frame.empty # corner case df = DataFrame({'A': [1., 2., 3.], @@ -202,16 +200,16 @@ def test_items(self): assert isinstance(v, Series) assert (df[k] == v).all() - def test_iter(self): - assert tm.equalContents(list(self.frame), self.frame.columns) + def test_iter(self, float_frame): + assert tm.equalContents(list(float_frame), float_frame.columns) - def test_iterrows(self): - for k, v in self.frame.iterrows(): - exp = self.frame.loc[k] + def test_iterrows(self, float_frame, float_string_frame): + for k, v in float_frame.iterrows(): + exp = float_frame.loc[k] self._assert_series_equal(v, exp) - for k, v in self.mixed_frame.iterrows(): - exp = self.mixed_frame.loc[k] + for k, v in float_string_frame.iterrows(): + exp = float_string_frame.loc[k] self._assert_series_equal(v, exp) def test_iterrows_iso8601(self): @@ -226,11 +224,11 @@ def test_iterrows_iso8601(self): exp = s.loc[k] self._assert_series_equal(v, exp) - def test_itertuples(self): - for i, tup in enumerate(self.frame.itertuples()): + 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 = self.frame.iloc[i, :].reset_index(drop=True) + expected = float_frame.iloc[i, :].reset_index(drop=True) self._assert_series_equal(s, expected) df = self.klass({'floats': np.random.randn(5), @@ -289,11 +287,11 @@ def test_sequence_like_with_categorical(self): for c, col in df.iteritems(): str(s) - def test_len(self): - assert len(self.frame) == len(self.frame.index) + def test_len(self, float_frame): + assert len(float_frame) == len(float_frame.index) - def test_values(self): - frame = self.frame + def test_values(self, float_frame, float_string_frame): + frame = float_frame arr = frame.values frame_cols = frame.columns @@ -306,20 +304,20 @@ def test_values(self): assert value == frame[col][i] # mixed type - arr = self.mixed_frame[['foo', 'A']].values + arr = float_string_frame[['foo', 'A']].values assert arr[0, 0] == 'bar' - df = self.klass({'real': [1, 2, 3], 'complex': [1j, 2j, 3j]}) + df = self.klass({'complex': [1j, 2j, 3j], 'real': [1, 2, 3]}) arr = df.values assert arr[0, 0] == 1j # single block corner case - arr = self.frame[['A', 'B']].values - expected = self.frame.reindex(columns=['A', 'B']).values + arr = float_frame[['A', 'B']].values + expected = float_frame.reindex(columns=['A', 'B']).values assert_almost_equal(arr, expected) - def test_transpose(self): - frame = self.frame + def test_transpose(self, float_frame): + frame = float_frame dft = frame.T for idx, series in compat.iteritems(dft): for col, value in compat.iteritems(series): @@ -343,8 +341,8 @@ def test_swapaxes(self): self._assert_frame_equal(df, df.swapaxes(0, 0)) pytest.raises(ValueError, df.swapaxes, 2, 5) - def test_axis_aliases(self): - f = self.frame + def test_axis_aliases(self, float_frame): + f = float_frame # reg name expected = f.sum(axis=0) @@ -361,23 +359,23 @@ def test_class_axis(self): assert pydoc.getdoc(DataFrame.index) assert pydoc.getdoc(DataFrame.columns) - def test_more_values(self): - values = self.mixed_frame.values - assert values.shape[1] == len(self.mixed_frame.columns) + def test_more_values(self, float_string_frame): + values = float_string_frame.values + assert values.shape[1] == len(float_string_frame.columns) - def test_repr_with_mi_nat(self): + def test_repr_with_mi_nat(self, float_string_frame): df = self.klass({'X': [1, 2]}, index=[[pd.NaT, pd.Timestamp('20130101')], ['a', 'b']]) res = repr(df) exp = ' X\nNaT a 1\n2013-01-01 b 2' assert res == exp - def test_iteritems_names(self): - for k, v in compat.iteritems(self.mixed_frame): + def test_iteritems_names(self, float_string_frame): + for k, v in compat.iteritems(float_string_frame): assert v.name == k - def test_series_put_names(self): - series = self.mixed_frame._series + def test_series_put_names(self, float_string_frame): + series = float_string_frame._series for k, v in compat.iteritems(series): assert v.name == k @@ -408,36 +406,37 @@ def test_with_datetimelikes(self): tm.assert_series_equal(result, expected) -class TestDataFrameMisc(SharedWithSparse, TestData): +class TestDataFrameMisc(SharedWithSparse): klass = DataFrame # SharedWithSparse tests use generic, klass-agnostic assertion _assert_frame_equal = staticmethod(assert_frame_equal) _assert_series_equal = staticmethod(assert_series_equal) - def test_values(self): - self.frame.values[:, 0] = 5. - assert (self.frame.values[:, 0] == 5).all() + def test_values(self, float_frame): + float_frame.values[:, 0] = 5. + assert (float_frame.values[:, 0] == 5).all() - def test_as_matrix_deprecated(self): + def test_as_matrix_deprecated(self, float_frame): # GH18458 with tm.assert_produces_warning(FutureWarning): - result = self.frame.as_matrix(columns=self.frame.columns.tolist()) - expected = self.frame.values + cols = float_frame.columns.tolist() + result = float_frame.as_matrix(columns=cols) + expected = float_frame.values tm.assert_numpy_array_equal(result, expected) - def test_deepcopy(self): - cp = deepcopy(self.frame) + def test_deepcopy(self, float_frame): + cp = deepcopy(float_frame) series = cp['A'] series[:] = 10 for idx, value in compat.iteritems(series): - assert self.frame['A'][idx] != value + assert float_frame['A'][idx] != value - def test_transpose_get_view(self): - dft = self.frame.T + def test_transpose_get_view(self, float_frame): + dft = float_frame.T dft.values[:, 5:10] = 5 - assert (self.frame.values[5:10] == 5).all() + assert (float_frame.values[5:10] == 5).all() def test_inplace_return_self(self): # re #1893 diff --git a/pandas/tests/sparse/frame/conftest.py b/pandas/tests/sparse/frame/conftest.py new file mode 100644 index 0000000000000..f36b4e643d10b --- /dev/null +++ b/pandas/tests/sparse/frame/conftest.py @@ -0,0 +1,116 @@ +import pytest + +import numpy as np + +from pandas import SparseDataFrame, SparseArray, DataFrame, 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_frame.py b/pandas/tests/sparse/frame/test_frame.py index be5a1710119ee..30938966b5d1a 100644 --- a/pandas/tests/sparse/frame/test_frame.py +++ b/pandas/tests/sparse/frame/test_frame.py @@ -28,42 +28,6 @@ class TestSparseDataFrame(SharedWithSparse): _assert_frame_equal = staticmethod(tm.assert_sp_frame_equal) _assert_series_equal = staticmethod(tm.assert_sp_series_equal) - def setup_method(self, method): - 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') - self.mixed_frame = self.frame.copy(False) - self.mixed_frame['foo'] = pd.SparseArray(['bar'] * len(self.dates)) - - 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') @@ -73,8 +37,8 @@ def test_fill_value_when_combine_const(self): res = df.add(2, fill_value=0) tm.assert_sp_frame_equal(res, exp) - def test_values(self): - empty = self.empty.values + def test_values(self, empty_frame, float_frame): + empty = empty_frame.values assert empty.shape == (0, 0) no_cols = SparseDataFrame(index=np.arange(10)) @@ -85,28 +49,29 @@ def test_values(self): mat = no_index.values assert mat.shape == (0, 10) - def test_copy(self): - cp = self.frame.copy() + def test_copy(self, float_frame): + cp = float_frame.copy() assert isinstance(cp, SparseDataFrame) - tm.assert_sp_frame_equal(cp, self.frame) + 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(self.frame.index) + assert cp.index.identical(float_frame.index) - def test_constructor(self): - for col, series in compat.iteritems(self.frame): + 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(self.iframe['A'].sp_index, IntIndex) + assert isinstance(float_frame_int_kind['A'].sp_index, IntIndex) # constructed zframe from matrix above - assert self.zframe['A'].fill_value == 0 + assert float_frame_fill0['A'].fill_value == 0 tm.assert_numpy_array_equal(pd.SparseArray([1., 2., 3., 4., 5., 6.]), - self.zframe['A'].values) + float_frame_fill0['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) + float_frame_fill0['A'].to_dense().values) # construct no data sdf = SparseDataFrame(columns=np.arange(10), index=np.arange(10)) @@ -115,29 +80,29 @@ def test_constructor(self): # construct from nested dict data = {} - for c, s in compat.iteritems(self.frame): + for c, s in compat.iteritems(float_frame): data[c] = s.to_dict() sdf = SparseDataFrame(data) - tm.assert_sp_frame_equal(sdf, self.frame) + tm.assert_sp_frame_equal(sdf, float_frame) # TODO: test data is copied from inputs # init dict with different index - idx = self.frame.index[:5] + idx = float_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) + 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): - self.frame.reindex(idx, level=0) + float_frame.reindex(idx, level=0) - repr(self.frame) + repr(float_frame) def test_constructor_dict_order(self): # GH19018 @@ -151,24 +116,26 @@ def test_constructor_dict_order(self): expected = SparseDataFrame(data=d, columns=list('ab')) tm.assert_sp_frame_equal(frame, expected) - def test_constructor_ndarray(self): + def test_constructor_ndarray(self, float_frame): # no index or columns - sp = SparseDataFrame(self.frame.values) + sp = SparseDataFrame(float_frame.values) # 1d - sp = SparseDataFrame(self.data['A'], index=self.dates, columns=['A']) - tm.assert_sp_frame_equal(sp, self.frame.reindex(columns=['A'])) + 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 - pytest.raises(TypeError, self.frame.reindex, columns=['A'], + pytest.raises(TypeError, float_frame.reindex, columns=['A'], level=1) # wrong length index / columns with tm.assert_raises_regex(ValueError, "^Index length"): - SparseDataFrame(self.frame.values, index=self.frame.index[:-1]) + SparseDataFrame(float_frame.values, index=float_frame.index[:-1]) with tm.assert_raises_regex(ValueError, "^Column length"): - SparseDataFrame(self.frame.values, columns=self.frame.columns[:-1]) + SparseDataFrame(float_frame.values, + columns=float_frame.columns[:-1]) # GH 9272 def test_constructor_empty(self): @@ -176,10 +143,10 @@ def test_constructor_empty(self): assert len(sp.index) == 0 assert len(sp.columns) == 0 - def test_constructor_dataframe(self): - dense = self.frame.to_dense() + def test_constructor_dataframe(self, float_frame): + dense = float_frame.to_dense() sp = SparseDataFrame(dense) - tm.assert_sp_frame_equal(sp, self.frame) + tm.assert_sp_frame_equal(sp, float_frame) def test_constructor_convert_index_once(self): arr = np.array([1.5, 2.5, 3.5]) @@ -292,12 +259,13 @@ def test_dtypes(self): expected = Series({'float64': 4}) tm.assert_series_equal(result, expected) - def test_shape(self): + def test_shape(self, float_frame, float_frame_int_kind, + float_frame_fill0, float_frame_fill2): # see gh-10452 - assert self.frame.shape == (10, 4) - assert self.iframe.shape == (10, 4) - assert self.zframe.shape == (10, 4) - assert self.fill_frame.shape == (10, 4) + 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)) @@ -306,12 +274,14 @@ def test_str(self): sdf = df.to_sparse() str(sdf) - def test_array_interface(self): - res = np.sqrt(self.frame) - dres = np.sqrt(self.frame.to_dense()) + 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): + 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) @@ -319,7 +289,10 @@ def _test_roundtrip(frame, orig): tm.assert_frame_equal(result.to_dense(), orig, check_dtype=False) _test_roundtrip(SparseDataFrame(), DataFrame()) - self._check_all(_test_roundtrip) + _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], @@ -353,17 +326,17 @@ def test_density(self): def test_sparse_to_dense(self): pass - def test_sparse_series_ops(self): - self._check_frame_ops(self.frame) + def test_sparse_series_ops(self, float_frame): + self._check_frame_ops(float_frame) - def test_sparse_series_ops_i(self): - self._check_frame_ops(self.iframe) + 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): - self._check_frame_ops(self.zframe) + def test_sparse_series_ops_z(self, float_frame_fill0): + self._check_frame_ops(float_frame_fill0) - def test_sparse_series_ops_fill(self): - self._check_frame_ops(self.fill_frame) + def test_sparse_series_ops_fill(self, float_frame_fill2): + self._check_frame_ops(float_frame_fill2) def _check_frame_ops(self, frame): @@ -417,18 +390,18 @@ def _compare_to_dense(a, b, da, db, op): _compare_to_dense(s, frame, s, frame.to_dense(), op) # it works! - result = self.frame + self.frame.loc[:, ['A', 'B']] # noqa + result = frame + frame.loc[:, ['A', 'B']] # noqa - def test_op_corners(self): - empty = self.empty + self.empty + def test_op_corners(self, float_frame, empty_frame): + empty = empty_frame + empty_frame assert empty.empty - foo = self.frame + self.empty + foo = float_frame + empty_frame assert isinstance(foo.index, DatetimeIndex) - tm.assert_frame_equal(foo, self.frame * np.nan) + tm.assert_frame_equal(foo, float_frame * np.nan) - foo = self.empty + self.frame - tm.assert_frame_equal(foo, self.frame * np.nan) + foo = empty_frame + float_frame + tm.assert_frame_equal(foo, float_frame * np.nan) def test_scalar_ops(self): pass @@ -443,12 +416,12 @@ def test_getitem(self): pytest.raises(Exception, sdf.__getitem__, ['a', 'd']) - def test_iloc(self): + def test_iloc(self, float_frame): - # 2227 - result = self.frame.iloc[:, 0] + # GH 2227 + result = float_frame.iloc[:, 0] assert isinstance(result, SparseSeries) - tm.assert_sp_series_equal(result, self.frame['A']) + tm.assert_sp_series_equal(result, float_frame['A']) # preserve sparse index type. #2251 data = {'A': [0, 1]} @@ -456,22 +429,22 @@ def test_iloc(self): tm.assert_class_equal(iframe['A'].sp_index, iframe.iloc[:, 0].sp_index) - def test_set_value(self): + def test_set_value(self, float_frame): # ok, as the index gets converted to object - frame = self.frame.copy() + 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 = self.frame + res = float_frame res.index = res.index.astype(object) with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - res = self.frame.set_value('foobar', 'B', 1.5) - assert res is not self.frame + 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): @@ -482,38 +455,42 @@ def test_set_value(self): res2 = res.set_value('foobar', 'qux', 1.5) assert res2 is not res tm.assert_index_equal(res2.columns, - pd.Index(list(self.frame.columns) + ['qux'])) + 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): + def test_fancy_index_misc(self, float_frame): # axis = 0 - sliced = self.frame.iloc[-2:, :] - expected = self.frame.reindex(index=self.frame.index[-2:]) + sliced = float_frame.iloc[-2:, :] + expected = float_frame.reindex(index=float_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:]) + 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): + def test_getitem_overload(self, float_frame): # slicing - sl = self.frame[:20] - tm.assert_sp_frame_equal(sl, self.frame.reindex(self.frame.index[:20])) + sl = float_frame[:20] + tm.assert_sp_frame_equal(sl, + float_frame.reindex(float_frame.index[:20])) # boolean indexing - d = self.frame.index[5] - indexer = self.frame.index > d + d = float_frame.index[5] + indexer = float_frame.index > d - subindex = self.frame.index[indexer] - subframe = self.frame[indexer] + subindex = float_frame.index[indexer] + subframe = float_frame[indexer] tm.assert_index_equal(subindex, subframe.index) - pytest.raises(Exception, self.frame.__getitem__, indexer[:-1]) + pytest.raises(Exception, float_frame.__getitem__, indexer[:-1]) - def test_setitem(self): + 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) @@ -566,24 +543,27 @@ def _check_frame(frame, orig): frame['K'] = frame.default_fill_value assert len(frame['K'].sp_values) == 0 - self._check_all(_check_frame) + _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) - def test_setitem_corner(self): - self.frame['a'] = self.frame['B'] - tm.assert_sp_series_equal(self.frame['a'], self.frame['B'], + 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): - arr = self.frame['B'] + def test_setitem_array(self, float_frame): + arr = float_frame['B'] - self.frame['E'] = arr - tm.assert_sp_series_equal(self.frame['E'], self.frame['B'], + float_frame['E'] = arr + tm.assert_sp_series_equal(float_frame['E'], float_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), + 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): @@ -595,44 +575,44 @@ def test_setitem_chained_no_consolidate(self): sdf[0][1] = 2 assert len(sdf._data.blocks) == 2 - def test_delitem(self): - A = self.frame['A'] - C = self.frame['C'] + def test_delitem(self, float_frame): + A = float_frame['A'] + C = float_frame['C'] - del self.frame['B'] - assert 'B' not in self.frame - tm.assert_sp_series_equal(self.frame['A'], A) - tm.assert_sp_series_equal(self.frame['C'], 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 self.frame['D'] - assert 'D' not in self.frame + del float_frame['D'] + assert 'D' not in float_frame - del self.frame['A'] - assert 'A' not in self.frame + del float_frame['A'] + assert 'A' not in float_frame - def test_set_columns(self): - self.frame.columns = self.frame.columns - pytest.raises(Exception, setattr, self.frame, 'columns', - self.frame.columns[:-1]) + def test_set_columns(self, float_frame): + float_frame.columns = float_frame.columns + pytest.raises(Exception, setattr, float_frame, 'columns', + float_frame.columns[:-1]) - def test_set_index(self): - self.frame.index = self.frame.index - pytest.raises(Exception, setattr, self.frame, 'index', - self.frame.index[:-1]) + def test_set_index(self, float_frame): + float_frame.index = float_frame.index + pytest.raises(Exception, setattr, float_frame, 'index', + float_frame.index[:-1]) - def test_append(self): - a = self.frame[:5] - b = self.frame[5:] + def test_append(self, float_frame): + a = float_frame[:5] + b = float_frame[5:] appended = a.append(b) - tm.assert_sp_frame_equal(appended, self.frame, exact_indices=False) + tm.assert_sp_frame_equal(appended, float_frame, exact_indices=False) - a = self.frame.iloc[:5, :3] - b = self.frame.iloc[5:] + 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], self.frame.iloc[:, :3], + tm.assert_sp_frame_equal(appended.iloc[:, :3], float_frame.iloc[:, :3], exact_indices=False) a = a[['B', 'C', 'A']].head(2) @@ -713,9 +693,9 @@ def test_astype_bool(self): assert res['A'].dtype == np.bool assert res['B'].dtype == np.bool - def test_fillna(self): - df = self.zframe.reindex(lrange(5)) - dense = self.zorig.reindex(lrange(5)) + 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) @@ -795,45 +775,48 @@ def test_sparse_frame_fillna_limit(self): 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")) + 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 = self.frame.rename(columns=lambda x: '%s%d' % (x, len(x))) + 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=self.dates) + expected = SparseDataFrame(data, index=float_frame.index) 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_corr(self, float_frame): + res = float_frame.corr() + tm.assert_frame_equal(res, float_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_describe(self, float_frame): + float_frame['foo'] = np.nan + float_frame.get_dtype_counts() + str(float_frame) + desc = float_frame.describe() # noqa - def test_join(self): - left = self.frame.loc[:, ['A', 'B']] - right = self.frame.loc[:, ['C', 'D']] + 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, self.frame, exact_indices=False) + tm.assert_sp_frame_equal(joined, float_frame, exact_indices=False) - right = self.frame.loc[:, ['B', 'D']] + right = float_frame.loc[:, ['B', 'D']] pytest.raises(Exception, left.join, right) with tm.assert_raises_regex(ValueError, 'Other Series must have a name'): - self.frame.join(Series( - np.random.randn(len(self.frame)), index=self.frame.index)) + float_frame.join(Series( + np.random.randn(len(float_frame)), index=float_frame.index)) - def test_reindex(self): + def test_reindex(self, float_frame, float_frame_int_kind, + float_frame_fill0, float_frame_fill2): def _check_frame(frame): index = frame.index @@ -876,26 +859,27 @@ def _check_frame(frame): frame.default_fill_value) assert np.isnan(reindexed['Z'].sp_values).all() - _check_frame(self.frame) - _check_frame(self.iframe) - _check_frame(self.zframe) - _check_frame(self.fill_frame) + _check_frame(float_frame) + _check_frame(float_frame_int_kind) + _check_frame(float_frame_fill0) + _check_frame(float_frame_fill2) # with copy=False - reindexed = self.frame.reindex(self.frame.index, copy=False) + reindexed = float_frame.reindex(float_frame.index, copy=False) reindexed['F'] = reindexed['A'] - assert 'F' in self.frame + assert 'F' in float_frame - reindexed = self.frame.reindex(self.frame.index) + reindexed = float_frame.reindex(float_frame.index) reindexed['G'] = reindexed['A'] - assert 'G' not in self.frame + assert 'G' not in float_frame - def test_reindex_fill_value(self): + def test_reindex_fill_value(self, float_frame_fill0, + float_frame_fill0_dense): 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) + 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): @@ -968,20 +952,27 @@ def test_reindex_method(self): with pytest.raises(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']) + 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): + 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() tm.assert_frame_equal(frame, dense_dm) tm.assert_frame_equal(dense_dm, orig, check_dtype=False) - self._check_all(_check) + _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_stack_sparse_frame(self): + def test_stack_sparse_frame(self, float_frame, float_frame_int_kind, + float_frame_fill0, float_frame_fill2): with catch_warnings(record=True): def _check(frame): @@ -995,14 +986,17 @@ def _check(frame): tm.assert_numpy_array_equal(from_dense_lp.values, from_sparse_lp.values) - _check(self.frame) - _check(self.iframe) + _check(float_frame) + _check(float_frame_int_kind) # for now - pytest.raises(Exception, _check, self.zframe) - pytest.raises(Exception, _check, self.fill_frame) + pytest.raises(Exception, _check, float_frame_fill0) + pytest.raises(Exception, _check, float_frame_fill2) - def test_transpose(self): + 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 @@ -1013,9 +1007,14 @@ def _check(frame, orig): 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) + _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): + 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) @@ -1042,32 +1041,29 @@ def _check(frame, orig): kind=frame.default_kind) tm.assert_frame_equal(shifted, exp) - self._check_all(_check) + _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): - dense_result = self.frame.to_dense().count() + def test_count(self, float_frame): + dense_result = float_frame.to_dense().count() - result = self.frame.count() + result = float_frame.count() tm.assert_series_equal(result, dense_result) - result = self.frame.count(axis=None) + result = float_frame.count(axis=None) tm.assert_series_equal(result, dense_result) - result = self.frame.count(axis=0) + result = float_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) + 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 _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)) @@ -1076,8 +1072,8 @@ def test_numpy_transpose(self): msg = "the 'axes' parameter is not supported" tm.assert_raises_regex(ValueError, msg, np.transpose, sdf, axes=1) - def test_combine_first(self): - df = self.frame + def test_combine_first(self, float_frame): + df = float_frame result = df[::2].combine_first(df) result2 = df[::2].combine_first(df.to_dense()) @@ -1088,8 +1084,8 @@ def test_combine_first(self): tm.assert_sp_frame_equal(result, result2) tm.assert_sp_frame_equal(result, expected) - def test_combine_add(self): - df = self.frame.to_dense() + 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 @@ -1214,51 +1210,42 @@ def test_comparison_op_scalar(self): class TestSparseDataFrameAnalytics(object): - def setup_method(self, method): - 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()) + def test_cumsum(self, float_frame): + expected = SparseDataFrame(float_frame.to_dense().cumsum()) - result = self.frame.cumsum() + result = float_frame.cumsum() tm.assert_sp_frame_equal(result, expected) - result = self.frame.cumsum(axis=None) + result = float_frame.cumsum(axis=None) tm.assert_sp_frame_equal(result, expected) - result = self.frame.cumsum(axis=0) + result = float_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()) + 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" tm.assert_raises_regex(ValueError, msg, np.cumsum, - self.frame, dtype=np.int64) + float_frame, dtype=np.int64) msg = "the 'out' parameter is not supported" tm.assert_raises_regex(ValueError, msg, np.cumsum, - self.frame, out=result) + float_frame, out=result) - def test_numpy_func_call(self): + 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)(self.frame) + getattr(np, func)(float_frame) - @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH 17386)', strict=True) def test_quantile(self): # GH 17386 @@ -1275,7 +1262,7 @@ def test_quantile(self): tm.assert_series_equal(result, dense_expected) tm.assert_sp_series_equal(result, sparse_expected) - @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH 17386)', strict=True) def test_quantile_multi(self): # GH 17386 From 79f7762c622c61e0dbf32575403206bcc9c402ab Mon Sep 17 00:00:00 2001 From: Troels Nielsen Date: Tue, 18 Sep 2018 16:47:31 +0200 Subject: [PATCH 22/87] BUG SeriesGroupBy.mean() overflowed on some integer array (#22653) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/dtypes/common.py | 27 +++++++++++++++++++++++++++ pandas/core/groupby/ops.py | 3 ++- pandas/tests/groupby/test_function.py | 9 +++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 39ed5d968707b..3a44b0260153c 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -768,6 +768,7 @@ Groupby/Resample/Rolling - Bug in :meth:`Resampler.apply` when passing postiional arguments to applied func (:issue:`14615`). - Bug in :meth:`Series.resample` when passing ``numpy.timedelta64`` to ``loffset`` kwarg (:issue:`7687`). - Bug in :meth:`Resampler.asfreq` when frequency of ``TimedeltaIndex`` is a subperiod of a new frequency (:issue:`13022`). +- Bug in :meth:`SeriesGroupBy.mean` when values were integral but could not fit inside of int64, overflowing instead. (:issue:`22487`) Sparse ^^^^^^ diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index b8cbb41501dd1..f6e7e87f1043b 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -90,6 +90,33 @@ def ensure_categorical(arr): return arr +def ensure_int64_or_float64(arr, copy=False): + """ + Ensure that an dtype array of some integer dtype + has an int64 dtype if possible + If it's not possible, potentially because of overflow, + convert the array to float64 instead. + + Parameters + ---------- + arr : array-like + The array whose data type we want to enforce. + copy: boolean + Whether to copy the original array or reuse + it in place, if possible. + + Returns + ------- + out_arr : The input array cast as int64 if + possible without overflow. + Otherwise the input array cast to float64. + """ + try: + return arr.astype('int64', copy=copy, casting='safe') + except TypeError: + return arr.astype('float64', copy=copy) + + def is_object_dtype(arr_or_dtype): """ Check whether an array-like or dtype is of the object dtype. diff --git a/pandas/core/groupby/ops.py b/pandas/core/groupby/ops.py index ba04ff3a3d3ee..d9f7b4d9c31c3 100644 --- a/pandas/core/groupby/ops.py +++ b/pandas/core/groupby/ops.py @@ -23,6 +23,7 @@ ensure_float64, ensure_platform_int, ensure_int64, + ensure_int64_or_float64, ensure_object, needs_i8_conversion, is_integer_dtype, @@ -471,7 +472,7 @@ def _cython_operation(self, kind, values, how, axis, min_count=-1, if (values == iNaT).any(): values = ensure_float64(values) else: - values = values.astype('int64', copy=False) + values = ensure_int64_or_float64(values) elif is_numeric and not is_complex_dtype(values): values = ensure_float64(values) else: diff --git a/pandas/tests/groupby/test_function.py b/pandas/tests/groupby/test_function.py index f8a0f1688c64e..775747ce0c6c1 100644 --- a/pandas/tests/groupby/test_function.py +++ b/pandas/tests/groupby/test_function.py @@ -1125,3 +1125,12 @@ def h(df, arg3): expected = pd.Series([4, 8, 12], index=pd.Int64Index([1, 2, 3])) tm.assert_series_equal(result, expected) + + +def test_groupby_mean_no_overflow(): + # Regression test for (#22487) + df = pd.DataFrame({ + "user": ["A", "A", "A", "A", "A"], + "connections": [4970, 4749, 4719, 4704, 18446744073699999744] + }) + assert df.groupby('user')['connections'].mean()['A'] == 3689348814740003840 From 1a12c41d201f56439510e683fadfed1218ea9067 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 18 Sep 2018 11:51:59 -0500 Subject: [PATCH 23/87] TST: Fail on warning (#22699) --- .travis.yml | 4 +- ...-numpydev.yaml => travis-37-numpydev.yaml} | 2 +- doc/source/contributing.rst | 57 + pandas/compat/__init__.py | 12 + pandas/compat/chainmap_impl.py | 9 +- pandas/core/algorithms.py | 5 +- pandas/core/arrays/datetimelike.py | 1 + pandas/core/arrays/integer.py | 10 +- pandas/core/common.py | 2 +- pandas/core/computation/eval.py | 1 + pandas/core/dtypes/inference.py | 7 +- pandas/core/frame.py | 6 +- pandas/core/groupby/generic.py | 2 +- pandas/core/indexes/base.py | 1 + pandas/core/internals/blocks.py | 1 + pandas/core/series.py | 5 +- pandas/core/window.py | 2 + pandas/io/common.py | 2 + pandas/io/html.py | 4 +- pandas/io/pickle.py | 3 +- pandas/tests/api/test_api.py | 13 +- pandas/tests/api/test_types.py | 13 +- pandas/tests/arithmetic/test_datetime64.py | 4 + pandas/tests/arithmetic/test_numeric.py | 3 +- pandas/tests/computation/test_eval.py | 20 +- pandas/tests/dtypes/test_generic.py | 3 +- pandas/tests/dtypes/test_inference.py | 8 +- pandas/tests/dtypes/test_missing.py | 3 +- pandas/tests/extension/base/dtype.py | 9 +- pandas/tests/extension/json/array.py | 7 +- pandas/tests/frame/test_analytics.py | 8 +- pandas/tests/frame/test_apply.py | 7 +- pandas/tests/frame/test_constructors.py | 3 +- pandas/tests/frame/test_convert_to.py | 3 +- pandas/tests/frame/test_indexing.py | 80 +- pandas/tests/frame/test_operators.py | 6 +- pandas/tests/frame/test_query_eval.py | 1 + pandas/tests/frame/test_reshape.py | 3 +- pandas/tests/frame/test_subclass.py | 40 +- pandas/tests/generic/test_generic.py | 13 +- pandas/tests/generic/test_panel.py | 4 +- pandas/tests/groupby/aggregate/test_cython.py | 7 +- pandas/tests/groupby/test_groupby.py | 83 +- pandas/tests/groupby/test_grouping.py | 37 +- pandas/tests/groupby/test_whitelist.py | 12 +- .../tests/indexes/datetimes/test_datetime.py | 3 +- pandas/tests/indexes/datetimes/test_ops.py | 2 +- pandas/tests/indexes/datetimes/test_tools.py | 37 +- pandas/tests/indexes/multi/test_duplicates.py | 5 +- pandas/tests/indexes/test_base.py | 2 + pandas/tests/indexes/timedeltas/test_ops.py | 2 +- .../indexes/timedeltas/test_timedelta.py | 4 +- pandas/tests/indexing/common.py | 57 +- .../indexing/test_chaining_and_caching.py | 18 +- pandas/tests/indexing/test_floats.py | 14 +- pandas/tests/indexing/test_iloc.py | 12 +- pandas/tests/indexing/test_indexing.py | 16 +- pandas/tests/indexing/test_indexing_slow.py | 1 + pandas/tests/indexing/test_ix.py | 16 +- pandas/tests/indexing/test_loc.py | 3 +- pandas/tests/indexing/test_multiindex.py | 179 +- pandas/tests/indexing/test_panel.py | 2 + pandas/tests/indexing/test_partial.py | 3 + pandas/tests/internals/test_internals.py | 2 +- pandas/tests/io/formats/test_to_excel.py | 4 +- .../tests/io/generate_legacy_storage_files.py | 3 +- pandas/tests/io/parser/compression.py | 26 +- pandas/tests/io/sas/test_sas7bdat.py | 2 + pandas/tests/io/test_common.py | 2 + pandas/tests/io/test_compression.py | 6 +- pandas/tests/io/test_excel.py | 3 + pandas/tests/io/test_packers.py | 11 +- pandas/tests/io/test_pickle.py | 12 +- pandas/tests/io/test_pytables.py | 47 +- pandas/tests/io/test_sql.py | 9 +- pandas/tests/io/test_stata.py | 24 +- pandas/tests/plotting/test_frame.py | 9 +- pandas/tests/plotting/test_hist_method.py | 17 +- pandas/tests/plotting/test_misc.py | 2 + pandas/tests/reshape/merge/test_join.py | 1 + pandas/tests/reshape/test_concat.py | 115 +- pandas/tests/reshape/test_reshape.py | 9 +- pandas/tests/series/indexing/test_datetime.py | 2 + pandas/tests/series/indexing/test_indexing.py | 2 + pandas/tests/series/test_analytics.py | 39 +- pandas/tests/series/test_api.py | 9 +- pandas/tests/series/test_constructors.py | 2 + pandas/tests/series/test_dtypes.py | 6 +- pandas/tests/sparse/frame/test_frame.py | 28 +- .../tests/sparse/frame/test_to_from_scipy.py | 12 +- pandas/tests/sparse/series/test_series.py | 3 + pandas/tests/test_downstream.py | 12 + pandas/tests/test_errors.py | 3 +- pandas/tests/test_expressions.py | 6 +- pandas/tests/test_multilevel.py | 10 +- pandas/tests/test_nanops.py | 7 +- pandas/tests/test_panel.py | 3553 ++++++++--------- pandas/tests/test_resample.py | 28 +- pandas/tests/test_window.py | 9 + pandas/tests/tseries/offsets/test_offsets.py | 3 + .../offsets/test_offsets_properties.py | 10 +- pandas/tests/tslibs/test_parsing.py | 3 + pandas/tests/util/test_hashing.py | 5 +- pandas/tseries/holiday.py | 4 +- pandas/util/testing.py | 10 +- setup.cfg | 3 +- 106 files changed, 2677 insertions(+), 2298 deletions(-) rename ci/{travis-36-numpydev.yaml => travis-37-numpydev.yaml} (95%) diff --git a/.travis.yml b/.travis.yml index 32e6d2eae90a7..76f4715a4abb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,7 +64,7 @@ matrix: # In allow_failures - dist: trusty env: - - JOB="3.6, NumPy dev" ENV_FILE="ci/travis-36-numpydev.yaml" TEST_ARGS="--skip-slow --skip-network" PANDAS_TESTING_MODE="deprecate" + - JOB="3.7, NumPy dev" ENV_FILE="ci/travis-37-numpydev.yaml" TEST_ARGS="--skip-slow --skip-network -W error" PANDAS_TESTING_MODE="deprecate" addons: apt: packages: @@ -79,7 +79,7 @@ matrix: - JOB="3.6, slow" ENV_FILE="ci/travis-36-slow.yaml" SLOW=true - dist: trusty env: - - JOB="3.6, NumPy dev" ENV_FILE="ci/travis-36-numpydev.yaml" TEST_ARGS="--skip-slow --skip-network" PANDAS_TESTING_MODE="deprecate" + - JOB="3.7, NumPy dev" ENV_FILE="ci/travis-37-numpydev.yaml" TEST_ARGS="--skip-slow --skip-network -W error" PANDAS_TESTING_MODE="deprecate" addons: apt: packages: diff --git a/ci/travis-36-numpydev.yaml b/ci/travis-37-numpydev.yaml similarity index 95% rename from ci/travis-36-numpydev.yaml rename to ci/travis-37-numpydev.yaml index aba28634edd0d..82c75b7c91b1f 100644 --- a/ci/travis-36-numpydev.yaml +++ b/ci/travis-37-numpydev.yaml @@ -2,7 +2,7 @@ name: pandas channels: - defaults dependencies: - - python=3.6* + - python=3.7* - pytz - Cython>=0.28.2 # universal diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst index 60bfd07961b38..65e151feeba67 100644 --- a/doc/source/contributing.rst +++ b/doc/source/contributing.rst @@ -632,6 +632,14 @@ Otherwise, you need to do it manually: warnings.warn('Use new_func instead.', FutureWarning, stacklevel=2) new_func() +You'll also need to + +1. write a new test that asserts a warning is issued when calling with the deprecated argument +2. Update all of pandas existing tests and code to use the new argument + +See :ref:`contributing.warnings` for more. + + .. _contributing.ci: Testing With Continuous Integration @@ -859,6 +867,55 @@ preferred if the inputs or logic are simple, with Hypothesis tests reserved for cases with complex logic or where there are too many combinations of options or subtle interactions to test (or think of!) all of them. +.. _contributing.warnings: + +Testing Warnings +~~~~~~~~~~~~~~~~ + +By default, one of pandas CI workers will fail if any unhandled warnings are emitted. + +If your change involves checking that a warning is actually emitted, use +``tm.assert_produces_warning(ExpectedWarning)``. + + +.. code-block:: python + + with tm.assert_prodcues_warning(FutureWarning): + df.some_operation() + +We prefer this to the ``pytest.warns`` context manager because ours checks that the warning's +stacklevel is set correctly. The stacklevel is what ensure the *user's* file name and line number +is printed in the warning, rather than something internal to pandas. It represents the number of +function calls from user code (e.g. ``df.some_operation()``) to the function that actually emits +the warning. Our linter will fail the build if you use ``pytest.warns`` in a test. + +If you have a test that would emit a warning, but you aren't actually testing the +warning itself (say because it's going to be removed in the future, or because we're +matching a 3rd-party library's behavior), then use ``pytest.mark.filterwarnings`` to +ignore the error. + +.. code-block:: python + + @pytest.mark.filterwarnings("ignore:msg:category") + def test_thing(self): + ... + +If the test generates a warning of class ``category`` whose message starts +with ``msg``, the warning will be ignored and the test will pass. + +If you need finer-grained control, you can use Python's usual +`warnings module `__ +to control whether a warning is ignored / raised at different places within +a single test. + +.. code-block:: python + + with warch.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + # Or use warnings.filterwarnings(...) + +Alternatively, consider breaking up the unit test. + Running the test suite ---------------------- diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py index 28a55133e68aa..1453725225e7d 100644 --- a/pandas/compat/__init__.py +++ b/pandas/compat/__init__.py @@ -38,6 +38,7 @@ import struct import inspect from collections import namedtuple +import collections PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] >= 3 @@ -135,6 +136,11 @@ def lfilter(*args, **kwargs): from importlib import reload reload = reload + Hashable = collections.abc.Hashable + Iterable = collections.abc.Iterable + Mapping = collections.abc.Mapping + Sequence = collections.abc.Sequence + Sized = collections.abc.Sized else: # Python 2 @@ -190,6 +196,12 @@ def get_range_parameters(data): reload = builtins.reload + Hashable = collections.Hashable + Iterable = collections.Iterable + Mapping = collections.Mapping + Sequence = collections.Sequence + Sized = collections.Sized + if PY2: def iteritems(obj, **kw): return obj.iteritems(**kw) diff --git a/pandas/compat/chainmap_impl.py b/pandas/compat/chainmap_impl.py index c4aa8c8d6ab30..3ea5414cc41eb 100644 --- a/pandas/compat/chainmap_impl.py +++ b/pandas/compat/chainmap_impl.py @@ -1,4 +1,11 @@ -from collections import MutableMapping +import sys + +PY3 = sys.version_info[0] >= 3 + +if PY3: + from collections.abc import MutableMapping +else: + from collections import MutableMapping try: from thread import get_ident diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index e5b6c84d37541..d39e9e08e2947 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -3,7 +3,7 @@ intended for public consumption """ from __future__ import division -from warnings import warn, catch_warnings +from warnings import warn, catch_warnings, simplefilter from textwrap import dedent import numpy as np @@ -91,7 +91,8 @@ def _ensure_data(values, dtype=None): # ignore the fact that we are casting to float # which discards complex parts - with catch_warnings(record=True): + with catch_warnings(): + simplefilter("ignore", np.ComplexWarning) values = ensure_float64(values) return values, 'float64', 'float64' diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 12e1dd1052e0b..69925ce1c520e 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -59,6 +59,7 @@ def cmp_method(self, other): # numpy will show a DeprecationWarning on invalid elementwise # comparisons, this will raise in the future with warnings.catch_warnings(record=True): + warnings.filterwarnings("ignore", "elementwise", FutureWarning) with np.errstate(all='ignore'): result = op(self.values, np.asarray(other)) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index aebc7a6a04ffc..e58109a25e1a5 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -5,7 +5,7 @@ from pandas._libs.lib import infer_dtype from pandas.util._decorators import cache_readonly -from pandas.compat import u, range +from pandas.compat import u, range, string_types from pandas.compat import set_function_name from pandas.core.dtypes.cast import astype_nansafe @@ -147,6 +147,11 @@ def coerce_to_array(values, dtype, mask=None, copy=False): dtype = values.dtype if dtype is not None: + if (isinstance(dtype, string_types) and + (dtype.startswith("Int") or dtype.startswith("UInt"))): + # Avoid DeprecationWarning from NumPy about np.dtype("Int64") + # https://github.com/numpy/numpy/pull/7476 + dtype = dtype.lower() if not issubclass(type(dtype), _IntegerDtype): try: dtype = _dtypes[str(np.dtype(dtype))] @@ -507,7 +512,8 @@ def cmp_method(self, other): # numpy will show a DeprecationWarning on invalid elementwise # comparisons, this will raise in the future - with warnings.catch_warnings(record=True): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "elementwise", FutureWarning) with np.errstate(all='ignore'): result = op(self._data, other) diff --git a/pandas/core/common.py b/pandas/core/common.py index 92e4e23ce958e..a6b05daf1d85d 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -356,7 +356,7 @@ def standardize_mapping(into): return partial( collections.defaultdict, into.default_factory) into = type(into) - if not issubclass(into, collections.Mapping): + if not issubclass(into, compat.Mapping): raise TypeError('unsupported type: {into}'.format(into=into)) elif into == collections.defaultdict: raise TypeError( diff --git a/pandas/core/computation/eval.py b/pandas/core/computation/eval.py index 434d7f6ccfe13..7025f3000eb5f 100644 --- a/pandas/core/computation/eval.py +++ b/pandas/core/computation/eval.py @@ -323,6 +323,7 @@ def eval(expr, parser='pandas', engine=None, truediv=True, # to use a non-numeric indexer try: with warnings.catch_warnings(record=True): + # TODO: Filter the warnings we actually care about here. target[assigner] = ret except (TypeError, IndexError): raise ValueError("Cannot assign expression output to target") diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index ed416c3ef857d..67f391615eedb 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -1,10 +1,9 @@ """ basic inference routines """ -import collections import re import numpy as np -from collections import Iterable from numbers import Number +from pandas import compat from pandas.compat import (PY2, string_types, text_type, string_and_binary_types, re_type) from pandas._libs import lib @@ -112,7 +111,7 @@ def _iterable_not_string(obj): False """ - return (isinstance(obj, collections.Iterable) and + return (isinstance(obj, compat.Iterable) and not isinstance(obj, string_types)) @@ -284,7 +283,7 @@ def is_list_like(obj): False """ - return (isinstance(obj, Iterable) and + return (isinstance(obj, compat.Iterable) and # we do not count strings/unicode/bytes as list-like not isinstance(obj, string_and_binary_types) and # exclude zero-dimensional numpy arrays, effectively scalars diff --git a/pandas/core/frame.py b/pandas/core/frame.py index bb08d4fa5582b..bb221ced9e6bd 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -417,9 +417,9 @@ def __init__(self, data=None, index=None, columns=None, dtype=None, copy=copy) # For data is list-like, or Iterable (will consume into list) - elif (isinstance(data, collections.Iterable) + elif (isinstance(data, compat.Iterable) and not isinstance(data, string_and_binary_types)): - if not isinstance(data, collections.Sequence): + if not isinstance(data, compat.Sequence): data = list(data) if len(data) > 0: if is_list_like(data[0]) and getattr(data[0], 'ndim', 1) == 1: @@ -7654,7 +7654,7 @@ def _to_arrays(data, columns, coerce_float=False, dtype=None): if isinstance(data[0], (list, tuple)): return _list_to_arrays(data, columns, coerce_float=coerce_float, dtype=dtype) - elif isinstance(data[0], collections.Mapping): + elif isinstance(data[0], compat.Mapping): return _list_of_dict_to_arrays(data, columns, coerce_float=coerce_float, dtype=dtype) elif isinstance(data[0], Series): diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 685635fb6854d..f15b1203a334e 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -758,7 +758,7 @@ def aggregate(self, func_or_funcs, *args, **kwargs): if isinstance(func_or_funcs, compat.string_types): return getattr(self, func_or_funcs)(*args, **kwargs) - if isinstance(func_or_funcs, collections.Iterable): + if isinstance(func_or_funcs, compat.Iterable): # Catch instances of lists / tuples # but not the class list / tuple itself. ret = self._aggregate_multiple_funcs(func_or_funcs, diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 487d3975a6219..b42bbdafcab45 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -98,6 +98,7 @@ def cmp_method(self, other): # numpy will show a DeprecationWarning on invalid elementwise # comparisons, this will raise in the future with warnings.catch_warnings(record=True): + warnings.filterwarnings("ignore", "elementwise", FutureWarning) with np.errstate(all='ignore'): result = op(self.values, np.asarray(other)) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index e735b35653cd4..6576db9f642a6 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -3490,6 +3490,7 @@ def _putmask_smart(v, m, n): # we ignore ComplexWarning here with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", np.ComplexWarning) nn_at = nn.astype(v.dtype) # avoid invalid dtype comparisons diff --git a/pandas/core/series.py b/pandas/core/series.py index 8f69de973e7a3..fdb9ef59c1d3e 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -6,7 +6,6 @@ # pylint: disable=E1101,E1103 # pylint: disable=W0703,W0622,W0613,W0201 -import collections import warnings from textwrap import dedent @@ -240,8 +239,8 @@ def __init__(self, data=None, index=None, dtype=None, name=None, raise TypeError("{0!r} type is unordered" "".format(data.__class__.__name__)) # If data is Iterable but not list-like, consume into list. - elif (isinstance(data, collections.Iterable) - and not isinstance(data, collections.Sized)): + elif (isinstance(data, compat.Iterable) + and not isinstance(data, compat.Sized)): data = list(data) else: diff --git a/pandas/core/window.py b/pandas/core/window.py index eed0e97f30dc9..66f48f403c941 100644 --- a/pandas/core/window.py +++ b/pandas/core/window.py @@ -2387,11 +2387,13 @@ def dataframe_from_int_dict(data, frame_template): if not arg2.columns.is_unique: raise ValueError("'arg2' columns are not unique") with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) X, Y = arg1.align(arg2, join='outer') X = X + 0 * Y Y = Y + 0 * X with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) res_columns = arg1.columns.union(arg2.columns) for col in res_columns: if col in X and col in Y: diff --git a/pandas/io/common.py b/pandas/io/common.py index 69cb9ed46419c..405911eda7e9e 100644 --- a/pandas/io/common.py +++ b/pandas/io/common.py @@ -386,6 +386,8 @@ def _get_handle(path_or_buf, mode, encoding=None, compression=None, # ZIP Compression elif compression == 'zip': zf = BytesZipFile(path_or_buf, mode) + # Ensure the container is closed as well. + handles.append(zf) if zf.mode == 'w': f = zf elif zf.mode == 'r': diff --git a/pandas/io/html.py b/pandas/io/html.py index cca27db00f48d..04534ff591a2c 100644 --- a/pandas/io/html.py +++ b/pandas/io/html.py @@ -6,7 +6,6 @@ import os import re import numbers -import collections from distutils.version import LooseVersion @@ -14,6 +13,7 @@ from pandas.errors import EmptyDataError from pandas.io.common import _is_url, urlopen, _validate_header_arg from pandas.io.parsers import TextParser +from pandas import compat from pandas.compat import (lrange, lmap, u, string_types, iteritems, raise_with_traceback, binary_type) from pandas import Series @@ -859,7 +859,7 @@ def _validate_flavor(flavor): flavor = 'lxml', 'bs4' elif isinstance(flavor, string_types): flavor = flavor, - elif isinstance(flavor, collections.Iterable): + elif isinstance(flavor, compat.Iterable): if not all(isinstance(flav, string_types) for flav in flavor): raise TypeError('Object of type {typ!r} is not an iterable of ' 'strings' diff --git a/pandas/io/pickle.py b/pandas/io/pickle.py index 6738daec9397c..9c219d7fd6997 100644 --- a/pandas/io/pickle.py +++ b/pandas/io/pickle.py @@ -160,7 +160,8 @@ def try_read(path, encoding=None): # GH 6899 try: with warnings.catch_warnings(record=True): - # We want to silencce any warnings about, e.g. moved modules. + # We want to silence any warnings about, e.g. moved modules. + warnings.simplefilter("ignore", Warning) return read_wrapper(lambda f: pkl.load(f)) except Exception: # reg/patched pickle diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 199700b304a4e..4033d46e161ad 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import sys -from warnings import catch_warnings import pytest import pandas as pd @@ -175,23 +174,23 @@ def test_get_store(self): class TestParser(object): + @pytest.mark.filterwarnings("ignore") def test_deprecation_access_func(self): - with catch_warnings(record=True): - pd.parser.na_values + pd.parser.na_values class TestLib(object): + @pytest.mark.filterwarnings("ignore") def test_deprecation_access_func(self): - with catch_warnings(record=True): - pd.lib.infer_dtype('foo') + pd.lib.infer_dtype('foo') class TestTSLib(object): + @pytest.mark.filterwarnings("ignore") def test_deprecation_access_func(self): - with catch_warnings(record=True): - pd.tslib.Timestamp('20160101') + pd.tslib.Timestamp('20160101') class TestTypes(object): diff --git a/pandas/tests/api/test_types.py b/pandas/tests/api/test_types.py index bd4891326c751..ed80c1414dbaa 100644 --- a/pandas/tests/api/test_types.py +++ b/pandas/tests/api/test_types.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- - +import sys import pytest -from warnings import catch_warnings - -import pandas from pandas.api import types from pandas.util import testing as tm @@ -59,7 +56,13 @@ def test_deprecated_from_api_types(self): def test_moved_infer_dtype(): + # del from sys.modules to ensure we try to freshly load. + # if this was imported from another test previously, we would + # not see the warning, since the import is otherwise cached. + sys.modules.pop("pandas.lib", None) + + with tm.assert_produces_warning(FutureWarning): + import pandas.lib - with catch_warnings(record=True): e = pandas.lib.infer_dtype('foo') assert e is not None diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index b19cc61a2999e..36bb0aca066fb 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -1803,6 +1803,10 @@ def test_dt64_with_DateOffsets(klass, normalize, cls_and_kwargs): offset_cls = getattr(pd.offsets, cls_name) with warnings.catch_warnings(record=True): + # pandas.errors.PerformanceWarning: Non-vectorized DateOffset being + # applied to Series or DatetimeIndex + # we aren't testing that here, so ignore. + warnings.simplefilter("ignore", PerformanceWarning) for n in [0, 5]: if (cls_name in ['WeekOfMonth', 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253'] and n == 0): diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index fcfc3994a88c8..0449212713048 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -4,7 +4,6 @@ # Specifically for numeric dtypes from decimal import Decimal import operator -from collections import Iterable import pytest import numpy as np @@ -12,7 +11,7 @@ import pandas as pd import pandas.util.testing as tm -from pandas.compat import PY3 +from pandas.compat import PY3, Iterable from pandas.core import ops from pandas import Timedelta, Series, Index, TimedeltaIndex diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py index 118b05d16ab09..eef8646e4d6d2 100644 --- a/pandas/tests/computation/test_eval.py +++ b/pandas/tests/computation/test_eval.py @@ -1,5 +1,4 @@ import warnings -from warnings import catch_warnings import operator from itertools import product @@ -924,12 +923,18 @@ def testit(r_idx_type, c_idx_type, index_name): # only test dt with dt, otherwise weird joins result args = product(['i', 'u', 's'], ['i', 'u', 's'], ('index', 'columns')) with warnings.catch_warnings(record=True): + # avoid warning about comparing strings and ints + warnings.simplefilter("ignore", RuntimeWarning) + for r_idx_type, c_idx_type, index_name in args: testit(r_idx_type, c_idx_type, index_name) # dt with dt args = product(['dt'], ['dt'], ('index', 'columns')) with warnings.catch_warnings(record=True): + # avoid warning about comparing strings and ints + warnings.simplefilter("ignore", RuntimeWarning) + for r_idx_type, c_idx_type, index_name in args: testit(r_idx_type, c_idx_type, index_name) @@ -1112,13 +1117,13 @@ def test_bool_ops_with_constants(self): exp = eval(ex) assert res == exp + @pytest.mark.filterwarnings("ignore::FutureWarning") def test_panel_fails(self): - with catch_warnings(record=True): - x = Panel(randn(3, 4, 5)) - y = Series(randn(10)) - with pytest.raises(NotImplementedError): - self.eval('x + y', - local_dict={'x': x, 'y': y}) + x = Panel(randn(3, 4, 5)) + y = Series(randn(10)) + with pytest.raises(NotImplementedError): + self.eval('x + y', + local_dict={'x': x, 'y': y}) def test_4d_ndarray_fails(self): x = randn(3, 4, 5, 6) @@ -1382,6 +1387,7 @@ def test_query_inplace(self): @pytest.mark.parametrize("invalid_target", [1, "cat", [1, 2], np.array([]), (1, 3)]) + @pytest.mark.filterwarnings("ignore::FutureWarning") def test_cannot_item_assign(self, invalid_target): msg = "Cannot assign expression output to target" expression = "a = 1 + 2" diff --git a/pandas/tests/dtypes/test_generic.py b/pandas/tests/dtypes/test_generic.py index 53f92b98f022e..38d1143f3838b 100644 --- a/pandas/tests/dtypes/test_generic.py +++ b/pandas/tests/dtypes/test_generic.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import numpy as np import pandas as pd from pandas.core.dtypes import generic as gt @@ -35,6 +35,7 @@ def test_abc_types(self): assert isinstance(pd.Series([1, 2, 3]), gt.ABCSeries) assert isinstance(self.df, gt.ABCDataFrame) with catch_warnings(record=True): + simplefilter('ignore', FutureWarning) assert isinstance(self.df.to_panel(), gt.ABCPanel) assert isinstance(self.sparse_series, gt.ABCSparseSeries) assert isinstance(self.sparse_array, gt.ABCSparseArray) diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index dc330666b4b6c..76cd6aabb93ae 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -5,7 +5,7 @@ related to inference and not otherwise tested in types/test_common.py """ -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import collections import re from datetime import datetime, date, timedelta, time @@ -20,6 +20,7 @@ DatetimeIndex, TimedeltaIndex, Timestamp, Panel, Period, Categorical, isna, Interval, DateOffset) +from pandas import compat from pandas.compat import u, PY2, StringIO, lrange from pandas.core.dtypes import inference from pandas.core.dtypes.common import ( @@ -226,7 +227,7 @@ class OldStyleClass(): pass c = OldStyleClass() - assert not isinstance(c, collections.Hashable) + assert not isinstance(c, compat.Hashable) assert inference.is_hashable(c) hash(c) # this will not raise @@ -1158,6 +1159,7 @@ def test_is_scalar_numpy_zerodim_arrays(self): assert not is_scalar(zerodim) assert is_scalar(lib.item_from_zerodim(zerodim)) + @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") def test_is_scalar_numpy_arrays(self): assert not is_scalar(np.array([])) assert not is_scalar(np.array([[]])) @@ -1176,6 +1178,7 @@ def test_is_scalar_pandas_containers(self): assert not is_scalar(DataFrame()) assert not is_scalar(DataFrame([[1]])) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) assert not is_scalar(Panel()) assert not is_scalar(Panel([[[1]]])) assert not is_scalar(Index([])) @@ -1210,6 +1213,7 @@ def test_nan_to_nat_conversions(): @td.skip_if_no_scipy +@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") def test_is_scipy_sparse(spmatrix): # noqa: F811 assert is_scipy_sparse(spmatrix([[0, 1]])) assert not is_scipy_sparse(np.array([1])) diff --git a/pandas/tests/dtypes/test_missing.py b/pandas/tests/dtypes/test_missing.py index ca9a2dc81fcc6..8f82db69a9213 100644 --- a/pandas/tests/dtypes/test_missing.py +++ b/pandas/tests/dtypes/test_missing.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import pytest -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import numpy as np from datetime import datetime from pandas.util import testing as tm @@ -94,6 +94,7 @@ def test_isna_isnull(self, isna_f): # panel with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) for p in [tm.makePanel(), tm.makePeriodPanel(), tm.add_nans(tm.makePanel())]: result = isna_f(p) diff --git a/pandas/tests/extension/base/dtype.py b/pandas/tests/extension/base/dtype.py index 02b7c9527769f..8d1f1cadcc23f 100644 --- a/pandas/tests/extension/base/dtype.py +++ b/pandas/tests/extension/base/dtype.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np import pandas as pd @@ -67,7 +69,12 @@ def test_check_dtype(self, data): expected = pd.Series([True, True, False, False], index=list('ABCD')) - result = df.dtypes == str(dtype) + # XXX: This should probably be *fixed* not ignored. + # See libops.scalar_compare + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = df.dtypes == str(dtype) + self.assert_series_equal(result, expected) expected = pd.Series([True, True, False, False], diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py index 980c245d55711..6ce0d63eb63ec 100644 --- a/pandas/tests/extension/json/array.py +++ b/pandas/tests/extension/json/array.py @@ -17,12 +17,13 @@ import numpy as np +from pandas import compat from pandas.core.dtypes.base import ExtensionDtype from pandas.core.arrays import ExtensionArray class JSONDtype(ExtensionDtype): - type = collections.Mapping + type = compat.Mapping name = 'json' try: na_value = collections.UserDict() @@ -79,7 +80,7 @@ def __getitem__(self, item): return self.data[item] elif isinstance(item, np.ndarray) and item.dtype == 'bool': return self._from_sequence([x for x, m in zip(self, item) if m]) - elif isinstance(item, collections.Iterable): + elif isinstance(item, compat.Iterable): # fancy indexing return type(self)([self.data[i] for i in item]) else: @@ -91,7 +92,7 @@ def __setitem__(self, key, value): self.data[key] = value else: if not isinstance(value, (type(self), - collections.Sequence)): + compat.Sequence)): # broadcast value value = itertools.cycle([value]) diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py index f06c8336373ca..52a52a1fd8752 100644 --- a/pandas/tests/frame/test_analytics.py +++ b/pandas/tests/frame/test_analytics.py @@ -116,8 +116,8 @@ def test_corr_int_and_boolean(self): 'a', 'b'], columns=['a', 'b']) for meth in ['pearson', 'kendall', 'spearman']: - # RuntimeWarning with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) result = df.corr(meth) tm.assert_frame_equal(result, expected) @@ -549,6 +549,8 @@ def test_mean(self): def test_product(self): self._check_stat_op('product', np.prod) + # TODO: Ensure warning isn't emitted in the first place + @pytest.mark.filterwarnings("ignore:All-NaN:RuntimeWarning") def test_median(self): def wrapper(x): if isna(x).any(): @@ -559,6 +561,7 @@ def wrapper(x): def test_min(self): with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) self._check_stat_op('min', np.min, check_dates=True) self._check_stat_op('min', np.min, frame=self.intframe) @@ -610,6 +613,7 @@ def test_cummax(self): def test_max(self): with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) self._check_stat_op('max', np.max, check_dates=True) self._check_stat_op('max', np.max, frame=self.intframe) @@ -1123,6 +1127,8 @@ def test_stats_mixed_type(self): self.mixed_frame.mean(1) self.mixed_frame.skew(1) + # TODO: Ensure warning isn't emitted in the first place + @pytest.mark.filterwarnings("ignore:All-NaN:RuntimeWarning") def test_median_corner(self): def wrapper(x): if isna(x).any(): diff --git a/pandas/tests/frame/test_apply.py b/pandas/tests/frame/test_apply.py index 1452e1ab8d98d..7b71240a34b5c 100644 --- a/pandas/tests/frame/test_apply.py +++ b/pandas/tests/frame/test_apply.py @@ -108,9 +108,9 @@ def test_apply_with_reduce_empty(self): assert x == [] def test_apply_deprecate_reduce(self): - with warnings.catch_warnings(record=True): - x = [] - self.empty.apply(x.append, axis=1, result_type='reduce') + x = [] + with tm.assert_produces_warning(FutureWarning): + self.empty.apply(x.append, axis=1, reduce=True) def test_apply_standard_nonunique(self): df = DataFrame( @@ -261,6 +261,7 @@ def test_apply_empty_infer_type(self): def _check(df, f): with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) test_res = f(np.array([], dtype='f8')) is_reduction = not isinstance(test_res, np.ndarray) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 6c84beb64e196..2f1c9e05a01b0 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -916,9 +916,8 @@ def test_constructor_list_of_lists(self): def test_constructor_sequence_like(self): # GH 3783 # collections.Squence like - import collections - class DummyContainer(collections.Sequence): + class DummyContainer(compat.Sequence): def __init__(self, lst): self._lst = lst diff --git a/pandas/tests/frame/test_convert_to.py b/pandas/tests/frame/test_convert_to.py index 2472022b862bc..a0e23d256c25b 100644 --- a/pandas/tests/frame/test_convert_to.py +++ b/pandas/tests/frame/test_convert_to.py @@ -110,9 +110,8 @@ def test_to_records_with_multindex(self): def test_to_records_with_Mapping_type(self): import email from email.parser import Parser - import collections - collections.Mapping.register(email.message.Message) + compat.Mapping.register(email.message.Message) headers = Parser().parsestr('From: \n' 'To: \n' diff --git a/pandas/tests/frame/test_indexing.py b/pandas/tests/frame/test_indexing.py index 96b2e98dd7e8d..2b93af357481a 100644 --- a/pandas/tests/frame/test_indexing.py +++ b/pandas/tests/frame/test_indexing.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import print_function -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from datetime import datetime, date, timedelta, time @@ -364,6 +364,7 @@ def test_getitem_ix_mixed_integer(self): assert_frame_equal(result, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[[1, 10]] expected = df.ix[Index([1, 10], dtype=object)] assert_frame_equal(result, expected) @@ -383,37 +384,45 @@ def test_getitem_ix_mixed_integer(self): def test_getitem_setitem_ix_negative_integers(self): with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = self.frame.ix[:, -1] assert_series_equal(result, self.frame['D']) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = self.frame.ix[:, [-1]] assert_frame_equal(result, self.frame[['D']]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = self.frame.ix[:, [-1, -2]] assert_frame_equal(result, self.frame[['D', 'C']]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) self.frame.ix[:, [-1]] = 0 assert (self.frame['D'] == 0).all() df = DataFrame(np.random.randn(8, 4)) # ix does label-based indexing when having an integer index with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) with pytest.raises(KeyError): df.ix[[-1]] with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) with pytest.raises(KeyError): df.ix[:, [-1]] # #1942 a = DataFrame(randn(20, 2), index=[chr(x + 65) for x in range(20)]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) a.ix[-1] = a.ix[-2] with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_series_equal(a.ix[-1], a.ix[-2], check_names=False) assert a.ix[-1].name == 'T' assert a.ix[-2].name == 'S' @@ -790,16 +799,19 @@ def test_getitem_fancy_2d(self): f = self.frame with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(f.ix[:, ['B', 'A']], f.reindex(columns=['B', 'A'])) subidx = self.frame.index[[5, 4, 1]] with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(f.ix[subidx, ['B', 'A']], f.reindex(index=subidx, columns=['B', 'A'])) # slicing rows, etc. with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(f.ix[5:10], f[5:10]) assert_frame_equal(f.ix[5:10, :], f[5:10]) assert_frame_equal(f.ix[:5, ['A', 'B']], @@ -808,22 +820,26 @@ def test_getitem_fancy_2d(self): # slice rows with labels, inclusive! with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) expected = f.ix[5:11] result = f.ix[f.index[5]:f.index[10]] assert_frame_equal(expected, result) # slice columns with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(f.ix[:, :2], f.reindex(columns=['A', 'B'])) # get view with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) exp = f.copy() f.ix[5:10].values[:] = 5 exp.values[5:10] = 5 assert_frame_equal(f, exp) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) pytest.raises(ValueError, f.ix.__getitem__, f > 0.5) def test_slice_floats(self): @@ -879,6 +895,7 @@ def test_setitem_fancy_2d(self): expected = frame.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame.ix[:, ['B', 'A']] = 1 expected['B'] = 1. expected['A'] = 1. @@ -894,6 +911,7 @@ def test_setitem_fancy_2d(self): values = randn(3, 2) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame.ix[subidx, ['B', 'A']] = values frame2.ix[[5, 4, 1], ['B', 'A']] = values @@ -907,12 +925,14 @@ def test_setitem_fancy_2d(self): frame = self.frame.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) expected1 = self.frame.copy() frame.ix[5:10] = 1. expected1.values[5:10] = 1. assert_frame_equal(frame, expected1) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) expected2 = self.frame.copy() arr = randn(5, len(frame.columns)) frame.ix[5:10] = arr @@ -921,6 +941,7 @@ def test_setitem_fancy_2d(self): # case 4 with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame = self.frame.copy() frame.ix[5:10, :] = 1. assert_frame_equal(frame, expected1) @@ -929,6 +950,7 @@ def test_setitem_fancy_2d(self): # case 5 with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame = self.frame.copy() frame2 = self.frame.copy() @@ -941,11 +963,13 @@ def test_setitem_fancy_2d(self): assert_frame_equal(frame, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame2.ix[:5, [0, 1]] = values assert_frame_equal(frame2, expected) # case 6: slice rows with labels, inclusive! with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame = self.frame.copy() expected = self.frame.copy() @@ -955,6 +979,7 @@ def test_setitem_fancy_2d(self): # case 7: slice columns with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame = self.frame.copy() frame2 = self.frame.copy() expected = self.frame.copy() @@ -997,6 +1022,7 @@ def test_fancy_setitem_int_labels(self): df = DataFrame(np.random.randn(10, 5), index=np.arange(0, 20, 2)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) tmp = df.copy() exp = df.copy() tmp.ix[[0, 2, 4]] = 5 @@ -1004,6 +1030,7 @@ def test_fancy_setitem_int_labels(self): assert_frame_equal(tmp, exp) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) tmp = df.copy() exp = df.copy() tmp.ix[6] = 5 @@ -1011,6 +1038,7 @@ def test_fancy_setitem_int_labels(self): assert_frame_equal(tmp, exp) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) tmp = df.copy() exp = df.copy() tmp.ix[:, 2] = 5 @@ -1024,21 +1052,25 @@ def test_fancy_getitem_int_labels(self): df = DataFrame(np.random.randn(10, 5), index=np.arange(0, 20, 2)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[[4, 2, 0], [2, 0]] expected = df.reindex(index=[4, 2, 0], columns=[2, 0]) assert_frame_equal(result, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[[4, 2, 0]] expected = df.reindex(index=[4, 2, 0]) assert_frame_equal(result, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[4] expected = df.xs(4) assert_series_equal(result, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[:, 3] expected = df[3] assert_series_equal(result, expected) @@ -1047,6 +1079,7 @@ def test_fancy_index_int_labels_exceptions(self): df = DataFrame(np.random.randn(10, 5), index=np.arange(0, 20, 2)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) # labels that aren't contained pytest.raises(KeyError, df.ix.__setitem__, @@ -1065,6 +1098,7 @@ def test_fancy_index_int_labels_exceptions(self): def test_setitem_fancy_mixed_2d(self): with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) self.mixed_frame.ix[:5, ['C', 'B', 'A']] = 5 result = self.mixed_frame.ix[:5, ['C', 'B', 'A']] assert (result.values == 5).all() @@ -1078,6 +1112,7 @@ def test_setitem_fancy_mixed_2d(self): # #1432 with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df = DataFrame({1: [1., 2., 3.], 2: [3, 4, 5]}) assert df._is_mixed_type @@ -1095,27 +1130,32 @@ def test_ix_align(self): df = df_orig.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df.ix[:, 0] = b assert_series_equal(df.ix[:, 0].reindex(b.index), b) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) dft = df_orig.T dft.ix[0, :] = b assert_series_equal(dft.ix[0, :].reindex(b.index), b) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df = df_orig.copy() df.ix[:5, 0] = b s = df.ix[:5, 0] assert_series_equal(s, b.reindex(s.index)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) dft = df_orig.T dft.ix[0, :5] = b s = dft.ix[0, :5] assert_series_equal(s, b.reindex(s.index)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df = df_orig.copy() idx = [0, 1, 3, 5] df.ix[idx, 0] = b @@ -1123,6 +1163,7 @@ def test_ix_align(self): assert_series_equal(s, b.reindex(s.index)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) dft = df_orig.T dft.ix[0, idx] = b s = dft.ix[0, idx] @@ -1134,6 +1175,7 @@ def test_ix_frame_align(self): df = df_orig.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df.ix[:3] = b out = b.ix[:3] assert_frame_equal(out, b) @@ -1141,12 +1183,14 @@ def test_ix_frame_align(self): b.sort_index(inplace=True) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df = df_orig.copy() df.ix[[0, 1, 2]] = b out = df.ix[[0, 1, 2]].reindex(b.index) assert_frame_equal(out, b) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df = df_orig.copy() df.ix[:3] = b out = df.ix[:3] @@ -1189,6 +1233,7 @@ def test_ix_multi_take_nonint_index(self): df = DataFrame(np.random.randn(3, 2), index=['x', 'y', 'z'], columns=['a', 'b']) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) rs = df.ix[[0], [0]] xp = df.reindex(['x'], columns=['a']) assert_frame_equal(rs, xp) @@ -1197,6 +1242,7 @@ def test_ix_multi_take_multiindex(self): df = DataFrame(np.random.randn(3, 2), index=['x', 'y', 'z'], columns=[['a', 'b'], ['1', '2']]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) rs = df.ix[[0], [0]] xp = df.reindex(['x'], columns=[('a', '1')]) assert_frame_equal(rs, xp) @@ -1206,14 +1252,17 @@ def test_ix_dup(self): df = DataFrame(np.random.randn(len(idx), 3), idx) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) sub = df.ix[:'d'] assert_frame_equal(sub, df) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) sub = df.ix['a':'c'] assert_frame_equal(sub, df.ix[0:4]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) sub = df.ix['b':'d'] assert_frame_equal(sub, df.ix[2:]) @@ -1222,48 +1271,57 @@ def test_getitem_fancy_1d(self): # return self if no slicing...for now with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert f.ix[:, :] is f # low dimensional slice with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) xs1 = f.ix[2, ['C', 'B', 'A']] xs2 = f.xs(f.index[2]).reindex(['C', 'B', 'A']) tm.assert_series_equal(xs1, xs2) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) ts1 = f.ix[5:10, 2] ts2 = f[f.columns[2]][5:10] tm.assert_series_equal(ts1, ts2) # positional xs with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) xs1 = f.ix[0] xs2 = f.xs(f.index[0]) tm.assert_series_equal(xs1, xs2) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) xs1 = f.ix[f.index[5]] xs2 = f.xs(f.index[5]) tm.assert_series_equal(xs1, xs2) # single column with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_series_equal(f.ix[:, 'A'], f['A']) # return view with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) exp = f.copy() exp.values[5] = 4 f.ix[5][:] = 4 tm.assert_frame_equal(exp, f) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) exp.values[:, 1] = 6 f.ix[:, 1][:] = 6 tm.assert_frame_equal(exp, f) # slice of mixed-frame with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) xs = self.mixed_frame.ix[5] exp = self.mixed_frame.xs(self.mixed_frame.index[5]) tm.assert_series_equal(xs, exp) @@ -1275,6 +1333,7 @@ def test_setitem_fancy_1d(self): expected = self.frame.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame.ix[2, ['C', 'B', 'A']] = [1., 2., 3.] expected['C'][2] = 1. expected['B'][2] = 2. @@ -1282,6 +1341,7 @@ def test_setitem_fancy_1d(self): assert_frame_equal(frame, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame2 = self.frame.copy() frame2.ix[2, [3, 2, 1]] = [1., 2., 3.] assert_frame_equal(frame, expected) @@ -1291,12 +1351,14 @@ def test_setitem_fancy_1d(self): expected = self.frame.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) vals = randn(5) expected.values[5:10, 2] = vals frame.ix[5:10, 2] = vals assert_frame_equal(frame, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame2 = self.frame.copy() frame2.ix[5:10, 'B'] = vals assert_frame_equal(frame, expected) @@ -1306,11 +1368,13 @@ def test_setitem_fancy_1d(self): expected = self.frame.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame.ix[4] = 5. expected.values[4] = 5. assert_frame_equal(frame, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame.ix[frame.index[4]] = 6. expected.values[4] = 6. assert_frame_equal(frame, expected) @@ -1320,6 +1384,7 @@ def test_setitem_fancy_1d(self): expected = self.frame.copy() with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) frame.ix[:, 'A'] = 7. expected['A'] = 7. assert_frame_equal(frame, expected) @@ -1830,6 +1895,7 @@ def test_single_element_ix_dont_upcast(self): assert issubclass(self.frame['E'].dtype.type, (int, np.integer)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = self.frame.ix[self.frame.index[5], 'E'] assert is_integer(result) @@ -1841,6 +1907,7 @@ def test_single_element_ix_dont_upcast(self): df["b"] = 666 with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[0, "b"] assert is_integer(result) result = df.loc[0, "b"] @@ -1848,6 +1915,7 @@ def test_single_element_ix_dont_upcast(self): expected = Series([666], [0], name='b') with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[[0], "b"] assert_series_equal(result, expected) result = df.loc[[0], "b"] @@ -1919,12 +1987,14 @@ def test_iloc_duplicates(self): result = df.iloc[0] with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result2 = df.ix[0] assert isinstance(result, Series) assert_almost_equal(result.values, df.values[0]) assert_series_equal(result, result2) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.T.iloc[:, 0] result2 = df.T.ix[:, 0] assert isinstance(result, Series) @@ -1937,16 +2007,19 @@ def test_iloc_duplicates(self): index=[['i', 'i', 'j'], ['X', 'X', 'Y']]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) rs = df.iloc[0] xp = df.ix[0] assert_series_equal(rs, xp) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) rs = df.iloc[:, 0] xp = df.T.ix[0] assert_series_equal(rs, xp) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) rs = df.iloc[:, [0]] xp = df.ix[:, [0]] assert_frame_equal(rs, xp) @@ -2168,6 +2241,7 @@ def test_getitem_ix_float_duplicates(self): expect = df.iloc[1:] assert_frame_equal(df.loc[0.2], expect) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(df.ix[0.2], expect) expect = df.iloc[1:, 0] @@ -2177,6 +2251,7 @@ def test_getitem_ix_float_duplicates(self): expect = df.iloc[1:] assert_frame_equal(df.loc[0.2], expect) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(df.ix[0.2], expect) expect = df.iloc[1:, 0] @@ -2187,6 +2262,7 @@ def test_getitem_ix_float_duplicates(self): expect = df.iloc[1:-1] assert_frame_equal(df.loc[0.2], expect) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(df.ix[0.2], expect) expect = df.iloc[1:-1, 0] @@ -2196,6 +2272,7 @@ def test_getitem_ix_float_duplicates(self): expect = df.iloc[[1, -1]] assert_frame_equal(df.loc[0.2], expect) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) assert_frame_equal(df.ix[0.2], expect) expect = df.iloc[[1, -1], 0] @@ -2411,6 +2488,7 @@ def test_index_namedtuple(self): df = DataFrame([(1, 2), (3, 4)], index=index, columns=["A", "B"]) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[IndexType("foo", "bar")]["A"] assert result == 1 diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index da4424b1ae626..97c94e1134cc8 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -209,6 +209,8 @@ def _check_unary_op(op): @pytest.mark.parametrize('op,res', [('__eq__', False), ('__ne__', True)]) + # not sure what's correct here. + @pytest.mark.filterwarnings("ignore:elementwise:FutureWarning") def test_logical_typeerror_with_non_valid(self, op, res): # we are comparing floats vs a string result = getattr(self.frame, op)('foo') @@ -278,7 +280,9 @@ def test_pos_numeric(self, df): assert_series_equal(+df['a'], df['a']) @pytest.mark.parametrize('df', [ - pd.DataFrame({'a': ['a', 'b']}), + # numpy changing behavior in the future + pytest.param(pd.DataFrame({'a': ['a', 'b']}), + marks=[pytest.mark.filterwarnings("ignore")]), pd.DataFrame({'a': np.array([-1, 2], dtype=object)}), pd.DataFrame({'a': [Decimal('-1.0'), Decimal('2.0')]}), ]) diff --git a/pandas/tests/frame/test_query_eval.py b/pandas/tests/frame/test_query_eval.py index 3be7ad12db883..3c6f0f0b2ab94 100644 --- a/pandas/tests/frame/test_query_eval.py +++ b/pandas/tests/frame/test_query_eval.py @@ -360,6 +360,7 @@ def to_series(mi, level): else: raise AssertionError("object must be a Series or Index") + @pytest.mark.filterwarnings("ignore::FutureWarning") def test_raise_on_panel_with_multiindex(self, parser, engine): p = tm.makePanel(7) p.items = tm.makeCustomIndex(len(p.items), nlevels=2) diff --git a/pandas/tests/frame/test_reshape.py b/pandas/tests/frame/test_reshape.py index 2f90d24f652ca..9f6735c7ba2bf 100644 --- a/pandas/tests/frame/test_reshape.py +++ b/pandas/tests/frame/test_reshape.py @@ -2,7 +2,7 @@ from __future__ import print_function -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from datetime import datetime import itertools @@ -56,6 +56,7 @@ def test_pivot(self): with catch_warnings(record=True): # pivot multiple columns + simplefilter("ignore", FutureWarning) wp = tm.makePanel() lp = wp.to_frame() df = lp.reset_index() diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index caaa311e9ee96..07289d897be62 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -2,7 +2,7 @@ from __future__ import print_function -from warnings import catch_warnings +import pytest import numpy as np from pandas import DataFrame, Series, MultiIndex, Panel, Index @@ -126,28 +126,28 @@ def test_indexing_sliced(self): tm.assert_series_equal(res, exp) assert isinstance(res, tm.SubclassedSeries) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_to_panel_expanddim(self): # GH 9762 - with catch_warnings(record=True): - class SubclassedFrame(DataFrame): - - @property - def _constructor_expanddim(self): - return SubclassedPanel - - class SubclassedPanel(Panel): - pass - - index = MultiIndex.from_tuples([(0, 0), (0, 1), (0, 2)]) - df = SubclassedFrame({'X': [1, 2, 3], 'Y': [4, 5, 6]}, index=index) - result = df.to_panel() - assert isinstance(result, SubclassedPanel) - expected = SubclassedPanel([[[1, 2, 3]], [[4, 5, 6]]], - items=['X', 'Y'], major_axis=[0], - minor_axis=[0, 1, 2], - dtype='int64') - tm.assert_panel_equal(result, expected) + class SubclassedFrame(DataFrame): + + @property + def _constructor_expanddim(self): + return SubclassedPanel + + class SubclassedPanel(Panel): + pass + + index = MultiIndex.from_tuples([(0, 0), (0, 1), (0, 2)]) + df = SubclassedFrame({'X': [1, 2, 3], 'Y': [4, 5, 6]}, index=index) + result = df.to_panel() + assert isinstance(result, SubclassedPanel) + expected = SubclassedPanel([[[1, 2, 3]], [[4, 5, 6]]], + items=['X', 'Y'], major_axis=[0], + minor_axis=[0, 1, 2], + dtype='int64') + tm.assert_panel_equal(result, expected) def test_subclass_attr_err_propagation(self): # GH 11808 diff --git a/pandas/tests/generic/test_generic.py b/pandas/tests/generic/test_generic.py index 533bff0384ad9..1652835de8228 100644 --- a/pandas/tests/generic/test_generic.py +++ b/pandas/tests/generic/test_generic.py @@ -2,7 +2,7 @@ # pylint: disable-msg=E1101,W0612 from copy import copy, deepcopy -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import pytest import numpy as np @@ -638,6 +638,7 @@ def test_sample(sel): s.sample(n=3, weights='weight_column') with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) panel = Panel(items=[0, 1, 2], major_axis=[2, 3, 4], minor_axis=[3, 4, 5]) with pytest.raises(ValueError): @@ -705,6 +706,7 @@ def test_sample(sel): # Test default axes with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) p = Panel(items=['a', 'b', 'c'], major_axis=[2, 4, 6], minor_axis=[1, 3, 5]) assert_panel_equal( @@ -743,6 +745,7 @@ def test_squeeze(self): for df in [tm.makeTimeDataFrame()]: tm.assert_frame_equal(df.squeeze(), df) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) for p in [tm.makePanel()]: tm.assert_panel_equal(p.squeeze(), p) @@ -751,6 +754,7 @@ def test_squeeze(self): tm.assert_series_equal(df.squeeze(), df['A']) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) p = tm.makePanel().reindex(items=['ItemA']) tm.assert_frame_equal(p.squeeze(), p['ItemA']) @@ -761,6 +765,7 @@ def test_squeeze(self): empty_series = Series([], name='five') empty_frame = DataFrame([empty_series]) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) empty_panel = Panel({'six': empty_frame}) [tm.assert_series_equal(empty_series, higher_dim.squeeze()) @@ -798,6 +803,7 @@ def test_transpose(self): tm.assert_frame_equal(df.transpose().transpose(), df) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) for p in [tm.makePanel()]: tm.assert_panel_equal(p.transpose(2, 0, 1) .transpose(1, 2, 0), p) @@ -820,6 +826,7 @@ def test_numpy_transpose(self): np.transpose, df, axes=1) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) p = tm.makePanel() tm.assert_panel_equal(np.transpose( np.transpose(p, axes=(2, 0, 1)), @@ -842,6 +849,7 @@ def test_take(self): indices = [-3, 2, 0, 1] with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) for p in [tm.makePanel()]: out = p.take(indices) expected = Panel(data=p.values.take(indices, axis=0), @@ -856,6 +864,7 @@ def test_take_invalid_kwargs(self): df = tm.makeTimeDataFrame() with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) p = tm.makePanel() for obj in (s, df, p): @@ -963,6 +972,7 @@ def test_equals(self): def test_describe_raises(self): with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) with pytest.raises(NotImplementedError): tm.makePanel().describe() @@ -996,6 +1006,7 @@ def test_pipe_tuple_error(self): def test_pipe_panel(self): with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) wp = Panel({'r1': DataFrame({"A": [1, 2, 3]})}) f = lambda x, y: x + y result = wp.pipe(f, 2) diff --git a/pandas/tests/generic/test_panel.py b/pandas/tests/generic/test_panel.py index 49cb773a1bd10..fe80b2af5ea63 100644 --- a/pandas/tests/generic/test_panel.py +++ b/pandas/tests/generic/test_panel.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # pylint: disable-msg=E1101,W0612 -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from pandas import Panel from pandas.util.testing import (assert_panel_equal, @@ -21,6 +21,7 @@ def test_to_xarray(self): from xarray import DataArray with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) p = tm.makePanel() result = p.to_xarray() @@ -51,6 +52,7 @@ def f(): def tester(self): f = getattr(super(TestPanel, self), t) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) f() return tester diff --git a/pandas/tests/groupby/aggregate/test_cython.py b/pandas/tests/groupby/aggregate/test_cython.py index 48a45e93e1e8e..d8a545b323674 100644 --- a/pandas/tests/groupby/aggregate/test_cython.py +++ b/pandas/tests/groupby/aggregate/test_cython.py @@ -25,7 +25,12 @@ 'var', 'sem', 'mean', - 'median', + pytest.param('median', + # ignore mean of empty slice + # and all-NaN + marks=[pytest.mark.filterwarnings( + "ignore::RuntimeWarning" + )]), 'prod', 'min', 'max', diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 9affd0241d028..483f814bc8383 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -3,7 +3,6 @@ import pytest -from warnings import catch_warnings from datetime import datetime from decimal import Decimal @@ -508,30 +507,30 @@ def test_frame_multi_key_function_list(): @pytest.mark.parametrize('op', [lambda x: x.sum(), lambda x: x.mean()]) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_groupby_multiple_columns(df, op): data = df grouped = data.groupby(['A', 'B']) - with catch_warnings(record=True): - result1 = op(grouped) - - expected = defaultdict(dict) - for n1, gp1 in data.groupby('A'): - for n2, gp2 in gp1.groupby('B'): - expected[n1][n2] = op(gp2.loc[:, ['C', 'D']]) - expected = {k: DataFrame(v) - for k, v in compat.iteritems(expected)} - expected = Panel.fromDict(expected).swapaxes(0, 1) - expected.major_axis.name, expected.minor_axis.name = 'A', 'B' - - # a little bit crude - for col in ['C', 'D']: - result_col = op(grouped[col]) - exp = expected[col] - pivoted = result1[col].unstack() - pivoted2 = result_col.unstack() - assert_frame_equal(pivoted.reindex_like(exp), exp) - assert_frame_equal(pivoted2.reindex_like(exp), exp) + result1 = op(grouped) + + expected = defaultdict(dict) + for n1, gp1 in data.groupby('A'): + for n2, gp2 in gp1.groupby('B'): + expected[n1][n2] = op(gp2.loc[:, ['C', 'D']]) + expected = {k: DataFrame(v) + for k, v in compat.iteritems(expected)} + expected = Panel.fromDict(expected).swapaxes(0, 1) + expected.major_axis.name, expected.minor_axis.name = 'A', 'B' + + # a little bit crude + for col in ['C', 'D']: + result_col = op(grouped[col]) + exp = expected[col] + pivoted = result1[col].unstack() + pivoted2 = result_col.unstack() + assert_frame_equal(pivoted.reindex_like(exp), exp) + assert_frame_equal(pivoted2.reindex_like(exp), exp) # test single series works the same result = data['C'].groupby([data['A'], data['B']]).mean() @@ -1032,6 +1031,8 @@ def test_groupby_mixed_type_columns(): tm.assert_frame_equal(result, expected) +# TODO: Ensure warning isn't emitted in the first place +@pytest.mark.filterwarnings("ignore:Mean of:RuntimeWarning") def test_cython_grouper_series_bug_noncontig(): arr = np.empty((100, 100)) arr.fill(np.nan) @@ -1181,11 +1182,11 @@ def test_groupby_nat_exclude(): pytest.raises(KeyError, grouped.get_group, pd.NaT) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_sparse_friendly(df): sdf = df[['C', 'D']].to_sparse() - with catch_warnings(record=True): - panel = tm.makePanel() - tm.add_nans(panel) + panel = tm.makePanel() + tm.add_nans(panel) def _check_work(gp): gp.mean() @@ -1201,29 +1202,29 @@ def _check_work(gp): # _check_work(panel.groupby(lambda x: x.month, axis=1)) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_panel_groupby(): - with catch_warnings(record=True): - panel = tm.makePanel() - tm.add_nans(panel) - grouped = panel.groupby({'ItemA': 0, 'ItemB': 0, 'ItemC': 1}, - axis='items') - agged = grouped.mean() - agged2 = grouped.agg(lambda x: x.mean('items')) + panel = tm.makePanel() + tm.add_nans(panel) + grouped = panel.groupby({'ItemA': 0, 'ItemB': 0, 'ItemC': 1}, + axis='items') + agged = grouped.mean() + agged2 = grouped.agg(lambda x: x.mean('items')) - tm.assert_panel_equal(agged, agged2) + tm.assert_panel_equal(agged, agged2) - tm.assert_index_equal(agged.items, Index([0, 1])) + tm.assert_index_equal(agged.items, Index([0, 1])) - grouped = panel.groupby(lambda x: x.month, axis='major') - agged = grouped.mean() + grouped = panel.groupby(lambda x: x.month, axis='major') + agged = grouped.mean() - exp = Index(sorted(list(set(panel.major_axis.month)))) - tm.assert_index_equal(agged.major_axis, exp) + exp = Index(sorted(list(set(panel.major_axis.month)))) + tm.assert_index_equal(agged.major_axis, exp) - grouped = panel.groupby({'A': 0, 'B': 0, 'C': 1, 'D': 1}, - axis='minor') - agged = grouped.mean() - tm.assert_index_equal(agged.minor_axis, Index([0, 1])) + grouped = panel.groupby({'A': 0, 'B': 0, 'C': 1, 'D': 1}, + axis='minor') + agged = grouped.mean() + tm.assert_index_equal(agged.minor_axis, Index([0, 1])) def test_groupby_2d_malformed(): diff --git a/pandas/tests/groupby/test_grouping.py b/pandas/tests/groupby/test_grouping.py index 737e8a805f3ce..e7c0881b11871 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -4,7 +4,6 @@ import pytest -from warnings import catch_warnings from pandas import (date_range, Timestamp, Index, MultiIndex, DataFrame, Series, CategoricalIndex) from pandas.util.testing import (assert_panel_equal, assert_frame_equal, @@ -557,15 +556,15 @@ def test_list_grouper_with_nat(self): class TestGetGroup(): + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_get_group(self): - with catch_warnings(record=True): - wp = tm.makePanel() - grouped = wp.groupby(lambda x: x.month, axis='major') + wp = tm.makePanel() + grouped = wp.groupby(lambda x: x.month, axis='major') - gp = grouped.get_group(1) - expected = wp.reindex( - major=[x for x in wp.major_axis if x.month == 1]) - assert_panel_equal(gp, expected) + gp = grouped.get_group(1) + expected = wp.reindex( + major=[x for x in wp.major_axis if x.month == 1]) + assert_panel_equal(gp, expected) # GH 5267 # be datelike friendly @@ -743,18 +742,18 @@ def test_multi_iter_frame(self, three_group): for key, group in grouped: pass + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_multi_iter_panel(self): - with catch_warnings(record=True): - wp = tm.makePanel() - grouped = wp.groupby([lambda x: x.month, lambda x: x.weekday()], - axis=1) - - for (month, wd), group in grouped: - exp_axis = [x - for x in wp.major_axis - if x.month == month and x.weekday() == wd] - expected = wp.reindex(major=exp_axis) - assert_panel_equal(group, expected) + wp = tm.makePanel() + grouped = wp.groupby([lambda x: x.month, lambda x: x.weekday()], + axis=1) + + for (month, wd), group in grouped: + exp_axis = [x + for x in wp.major_axis + if x.month == month and x.weekday() == wd] + expected = wp.reindex(major=exp_axis) + assert_panel_equal(group, expected) def test_dictify(self, df): dict(iter(df.groupby('A'))) diff --git a/pandas/tests/groupby/test_whitelist.py b/pandas/tests/groupby/test_whitelist.py index 3afc278f9bc93..ae033f7b3f251 100644 --- a/pandas/tests/groupby/test_whitelist.py +++ b/pandas/tests/groupby/test_whitelist.py @@ -133,11 +133,15 @@ def df_letters(): return df -@pytest.mark.parametrize( - "obj, whitelist", zip((df_letters(), df_letters().floats), - (df_whitelist, s_whitelist))) -def test_groupby_whitelist(df_letters, obj, whitelist): +@pytest.mark.parametrize("whitelist", [df_whitelist, s_whitelist]) +def test_groupby_whitelist(df_letters, whitelist): df = df_letters + if whitelist == df_whitelist: + # dataframe + obj = df_letters + else: + obj = df_letters['floats'] + gb = obj.groupby(df.letters) assert set(whitelist) == set(gb._apply_whitelist) diff --git a/pandas/tests/indexes/datetimes/test_datetime.py b/pandas/tests/indexes/datetimes/test_datetime.py index db3de0ceced0c..5ab32ee3863ae 100644 --- a/pandas/tests/indexes/datetimes/test_datetime.py +++ b/pandas/tests/indexes/datetimes/test_datetime.py @@ -1,4 +1,3 @@ -import warnings import sys import pytest @@ -201,7 +200,7 @@ def test_get_duplicates(self): idx = DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-02', '2000-01-03', '2000-01-03', '2000-01-04']) - with warnings.catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning): # Deprecated - see GH20239 result = idx.get_duplicates() diff --git a/pandas/tests/indexes/datetimes/test_ops.py b/pandas/tests/indexes/datetimes/test_ops.py index 6ccd310f33bbd..24d99abaf44a8 100644 --- a/pandas/tests/indexes/datetimes/test_ops.py +++ b/pandas/tests/indexes/datetimes/test_ops.py @@ -534,8 +534,8 @@ def test_shift(self): assert shifted[0] == self.rng[0] assert shifted.freq == self.rng.freq - # PerformanceWarning with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", pd.errors.PerformanceWarning) rng = date_range(START, END, freq=BMonthEnd()) shifted = rng.shift(1, freq=CDay()) assert shifted[0] == rng[0] + CDay() diff --git a/pandas/tests/indexes/datetimes/test_tools.py b/pandas/tests/indexes/datetimes/test_tools.py index bef9b73773f46..cc6db8f5854c8 100644 --- a/pandas/tests/indexes/datetimes/test_tools.py +++ b/pandas/tests/indexes/datetimes/test_tools.py @@ -1175,6 +1175,8 @@ def test_dayfirst(self, cache): class TestGuessDatetimeFormat(object): @td.skip_if_not_us_locale + @pytest.mark.filterwarnings("ignore:_timelex:DeprecationWarning") + # https://github.com/pandas-dev/pandas/issues/21322 def test_guess_datetime_format_for_array(self): expected_format = '%Y-%m-%d %H:%M:%S.%f' dt_string = datetime(2011, 12, 30, 0, 0, 0).strftime(expected_format) @@ -1573,12 +1575,20 @@ def test_parsers_timezone_minute_offsets_roundtrip(self, cache, dt_string, @pytest.fixture(params=['D', 's', 'ms', 'us', 'ns']) def units(request): + """Day and some time units. + + * D + * s + * ms + * us + * ns + """ return request.param @pytest.fixture def epoch_1960(): - # for origin as 1960-01-01 + """Timestamp at 1960-01-01.""" return Timestamp('1960-01-01') @@ -1587,12 +1597,25 @@ def units_from_epochs(): return list(range(5)) -@pytest.fixture(params=[epoch_1960(), - epoch_1960().to_pydatetime(), - epoch_1960().to_datetime64(), - str(epoch_1960())]) -def epochs(request): - return request.param +@pytest.fixture(params=['timestamp', 'pydatetime', 'datetime64', 'str_1960']) +def epochs(epoch_1960, request): + """Timestamp at 1960-01-01 in various forms. + + * pd.Timestamp + * datetime.datetime + * numpy.datetime64 + * str + """ + assert request.param in {'timestamp', 'pydatetime', 'datetime64', + "str_1960"} + if request.param == 'timestamp': + return epoch_1960 + elif request.param == 'pydatetime': + return epoch_1960.to_pydatetime() + elif request.param == "datetime64": + return epoch_1960.to_datetime64() + else: + return str(epoch_1960) @pytest.fixture diff --git a/pandas/tests/indexes/multi/test_duplicates.py b/pandas/tests/indexes/multi/test_duplicates.py index 1cdf0ca6e013e..54a12137c9457 100644 --- a/pandas/tests/indexes/multi/test_duplicates.py +++ b/pandas/tests/indexes/multi/test_duplicates.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import warnings from itertools import product import pytest @@ -241,7 +240,7 @@ def test_get_duplicates(): mi = MultiIndex.from_arrays([[101, a], [3.5, np.nan]]) assert not mi.has_duplicates - with warnings.catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning): # Deprecated - see GH20239 assert mi.get_duplicates().equals(MultiIndex.from_arrays([[], []])) @@ -257,7 +256,7 @@ def test_get_duplicates(): assert len(mi) == (n + 1) * (m + 1) assert not mi.has_duplicates - with warnings.catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning): # Deprecated - see GH20239 assert mi.get_duplicates().equals(MultiIndex.from_arrays( [[], []])) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index eab04419fe939..99a909849822b 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -715,6 +715,8 @@ def test_empty_fancy_raises(self, attr): pytest.raises(IndexError, index.__getitem__, empty_farr) @pytest.mark.parametrize("itm", [101, 'no_int']) + # FutureWarning from non-tuple sequence of nd indexing + @pytest.mark.filterwarnings("ignore::FutureWarning") def test_getitem_error(self, indices, itm): with pytest.raises(IndexError): indices[itm] diff --git a/pandas/tests/indexes/timedeltas/test_ops.py b/pandas/tests/indexes/timedeltas/test_ops.py index 2e257bb8a500a..d7bdd18f48523 100644 --- a/pandas/tests/indexes/timedeltas/test_ops.py +++ b/pandas/tests/indexes/timedeltas/test_ops.py @@ -334,7 +334,7 @@ def test_freq_setter_errors(self): idx.freq = '5D' # setting with a non-fixed frequency - msg = '<2 \* BusinessDays> is a non-fixed frequency' + msg = r'<2 \* BusinessDays> is a non-fixed frequency' with tm.assert_raises_regex(ValueError, msg): idx.freq = '2B' diff --git a/pandas/tests/indexes/timedeltas/test_timedelta.py b/pandas/tests/indexes/timedeltas/test_timedelta.py index d7745ffd94cd9..c329d8d15d729 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta.py @@ -1,5 +1,3 @@ -import warnings - import pytest import numpy as np @@ -147,7 +145,7 @@ def test_get_duplicates(self): idx = TimedeltaIndex(['1 day', '2 day', '2 day', '3 day', '3day', '4day']) - with warnings.catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning): # Deprecated - see GH20239 result = idx.get_duplicates() diff --git a/pandas/tests/indexing/common.py b/pandas/tests/indexing/common.py index cbf1bdbce9574..127548bdaf106 100644 --- a/pandas/tests/indexing/common.py +++ b/pandas/tests/indexing/common.py @@ -2,6 +2,7 @@ import itertools from warnings import catch_warnings, filterwarnings +import pytest import numpy as np from pandas.compat import lrange @@ -25,6 +26,7 @@ def _axify(obj, key, axis): return tuple(axes) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class Base(object): """ indexing comprehensive base class """ @@ -49,22 +51,20 @@ def setup_method(self, method): self.frame_uints = DataFrame(np.random.randn(4, 4), index=UInt64Index(lrange(0, 8, 2)), columns=UInt64Index(lrange(0, 12, 3))) - with catch_warnings(record=True): - self.panel_uints = Panel(np.random.rand(4, 4, 4), - items=UInt64Index(lrange(0, 8, 2)), - major_axis=UInt64Index(lrange(0, 12, 3)), - minor_axis=UInt64Index(lrange(0, 16, 4))) + self.panel_uints = Panel(np.random.rand(4, 4, 4), + items=UInt64Index(lrange(0, 8, 2)), + major_axis=UInt64Index(lrange(0, 12, 3)), + minor_axis=UInt64Index(lrange(0, 16, 4))) self.series_floats = Series(np.random.rand(4), index=Float64Index(range(0, 8, 2))) self.frame_floats = DataFrame(np.random.randn(4, 4), index=Float64Index(range(0, 8, 2)), columns=Float64Index(range(0, 12, 3))) - with catch_warnings(record=True): - self.panel_floats = Panel(np.random.rand(4, 4, 4), - items=Float64Index(range(0, 8, 2)), - major_axis=Float64Index(range(0, 12, 3)), - minor_axis=Float64Index(range(0, 16, 4))) + self.panel_floats = Panel(np.random.rand(4, 4, 4), + items=Float64Index(range(0, 8, 2)), + major_axis=Float64Index(range(0, 12, 3)), + minor_axis=Float64Index(range(0, 16, 4))) m_idces = [MultiIndex.from_product([[1, 2], [3, 4]]), MultiIndex.from_product([[5, 6], [7, 8]]), @@ -75,35 +75,31 @@ def setup_method(self, method): self.frame_multi = DataFrame(np.random.randn(4, 4), index=m_idces[0], columns=m_idces[1]) - with catch_warnings(record=True): - self.panel_multi = Panel(np.random.rand(4, 4, 4), - items=m_idces[0], - major_axis=m_idces[1], - minor_axis=m_idces[2]) + self.panel_multi = Panel(np.random.rand(4, 4, 4), + items=m_idces[0], + major_axis=m_idces[1], + minor_axis=m_idces[2]) self.series_labels = Series(np.random.randn(4), index=list('abcd')) self.frame_labels = DataFrame(np.random.randn(4, 4), index=list('abcd'), columns=list('ABCD')) - with catch_warnings(record=True): - self.panel_labels = Panel(np.random.randn(4, 4, 4), - items=list('abcd'), - major_axis=list('ABCD'), - minor_axis=list('ZYXW')) + self.panel_labels = Panel(np.random.randn(4, 4, 4), + items=list('abcd'), + major_axis=list('ABCD'), + minor_axis=list('ZYXW')) self.series_mixed = Series(np.random.randn(4), index=[2, 4, 'null', 8]) self.frame_mixed = DataFrame(np.random.randn(4, 4), index=[2, 4, 'null', 8]) - with catch_warnings(record=True): - self.panel_mixed = Panel(np.random.randn(4, 4, 4), - items=[2, 4, 'null', 8]) + self.panel_mixed = Panel(np.random.randn(4, 4, 4), + items=[2, 4, 'null', 8]) self.series_ts = Series(np.random.randn(4), index=date_range('20130101', periods=4)) self.frame_ts = DataFrame(np.random.randn(4, 4), index=date_range('20130101', periods=4)) - with catch_warnings(record=True): - self.panel_ts = Panel(np.random.randn(4, 4, 4), - items=date_range('20130101', periods=4)) + self.panel_ts = Panel(np.random.randn(4, 4, 4), + items=date_range('20130101', periods=4)) dates_rev = (date_range('20130101', periods=4) .sort_values(ascending=False)) @@ -111,14 +107,12 @@ def setup_method(self, method): index=dates_rev) self.frame_ts_rev = DataFrame(np.random.randn(4, 4), index=dates_rev) - with catch_warnings(record=True): - self.panel_ts_rev = Panel(np.random.randn(4, 4, 4), - items=dates_rev) + self.panel_ts_rev = Panel(np.random.randn(4, 4, 4), + items=dates_rev) self.frame_empty = DataFrame({}) self.series_empty = Series({}) - with catch_warnings(record=True): - self.panel_empty = Panel({}) + self.panel_empty = Panel({}) # form agglomerates for o in self._objs: @@ -175,6 +169,7 @@ def get_value(self, f, i, values=False): # v = v.__getitem__(a) # return v with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) return f.ix[i] def check_values(self, f, func, values=False): diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py index 0e396a3248e3f..a7e55cdf9936e 100644 --- a/pandas/tests/indexing/test_chaining_and_caching.py +++ b/pandas/tests/indexing/test_chaining_and_caching.py @@ -1,5 +1,3 @@ -from warnings import catch_warnings - import pytest import numpy as np @@ -366,22 +364,22 @@ def check(result, expected): result4 = df['A'].iloc[2] check(result4, expected) + @pytest.mark.filterwarnings("ignore::DeprecationWarning") + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_cache_updating(self): # GH 4939, make sure to update the cache on setitem df = tm.makeDataFrame() df['A'] # cache series - with catch_warnings(record=True): - df.ix["Hello Friend"] = df.ix[0] + df.ix["Hello Friend"] = df.ix[0] assert "Hello Friend" in df['A'].index assert "Hello Friend" in df['B'].index - with catch_warnings(record=True): - panel = tm.makePanel() - panel.ix[0] # get first item into cache - panel.ix[:, :, 'A+1'] = panel.ix[:, :, 'A'] + 1 - assert "A+1" in panel.ix[0].columns - assert "A+1" in panel.ix[1].columns + panel = tm.makePanel() + panel.ix[0] # get first item into cache + panel.ix[:, :, 'A+1'] = panel.ix[:, :, 'A'] + 1 + assert "A+1" in panel.ix[0].columns + assert "A+1" in panel.ix[1].columns # 5216 # make sure that we don't try to set a dead cache diff --git a/pandas/tests/indexing/test_floats.py b/pandas/tests/indexing/test_floats.py index ba1f1de21871f..3773b432135b9 100644 --- a/pandas/tests/indexing/test_floats.py +++ b/pandas/tests/indexing/test_floats.py @@ -10,6 +10,9 @@ import pandas.util.testing as tm +ignore_ix = pytest.mark.filterwarnings("ignore:\\n.ix:DeprecationWarning") + + class TestFloatIndexers(object): def check(self, result, original, indexer, getitem): @@ -57,6 +60,7 @@ def f(): s.iloc[3.0] = 0 pytest.raises(TypeError, f) + @ignore_ix def test_scalar_non_numeric(self): # GH 4892 @@ -145,6 +149,7 @@ def f(): s[3] pytest.raises(TypeError, lambda: s[3.0]) + @ignore_ix def test_scalar_with_mixed(self): s2 = Series([1, 2, 3], index=['a', 'b', 'c']) @@ -202,6 +207,7 @@ def f(): expected = 3 assert result == expected + @ignore_ix def test_scalar_integer(self): # test how scalar float indexers work on int indexes @@ -254,6 +260,7 @@ def compare(x, y): # coerce to equal int assert 3.0 in s + @ignore_ix def test_scalar_float(self): # scalar float indexers work on a float index @@ -269,8 +276,7 @@ def test_scalar_float(self): (lambda x: x, True)]: # getting - with catch_warnings(record=True): - result = idxr(s)[indexer] + result = idxr(s)[indexer] self.check(result, s, 3, getitem) # setting @@ -305,6 +311,7 @@ def g(): s2.iloc[3.0] = 0 pytest.raises(TypeError, g) + @ignore_ix def test_slice_non_numeric(self): # GH 4892 @@ -356,6 +363,7 @@ def f(): idxr(s)[l] = 0 pytest.raises(TypeError, f) + @ignore_ix def test_slice_integer(self): # same as above, but for Integer based indexes @@ -483,6 +491,7 @@ def f(): pytest.raises(TypeError, f) + @ignore_ix def test_slice_integer_frame_getitem(self): # similar to above, but on the getitem dim (of a DataFrame) @@ -554,6 +563,7 @@ def f(): with catch_warnings(record=True): f(lambda x: x.ix) + @ignore_ix def test_slice_float(self): # same as above, but for floats diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py index 3dcfe6a68ad9f..538d9706d54d6 100644 --- a/pandas/tests/indexing/test_iloc.py +++ b/pandas/tests/indexing/test_iloc.py @@ -2,7 +2,7 @@ import pytest -from warnings import catch_warnings +from warnings import catch_warnings, filterwarnings, simplefilter import numpy as np import pandas as pd @@ -388,45 +388,53 @@ def test_iloc_getitem_frame(self): result = df.iloc[2] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) exp = df.ix[4] tm.assert_series_equal(result, exp) result = df.iloc[2, 2] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) exp = df.ix[4, 4] assert result == exp # slice result = df.iloc[4:8] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[8:14] tm.assert_frame_equal(result, expected) result = df.iloc[:, 2:3] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[:, 4:5] tm.assert_frame_equal(result, expected) # list of integers result = df.iloc[[0, 1, 3]] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[[0, 2, 6]] tm.assert_frame_equal(result, expected) result = df.iloc[[0, 1, 3], [0, 1]] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[[0, 2, 6], [0, 2]] tm.assert_frame_equal(result, expected) # neg indices result = df.iloc[[-1, 1, 3], [-1, 1]] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[[18, 2, 6], [6, 2]] tm.assert_frame_equal(result, expected) # dups indices result = df.iloc[[-1, -1, 1, 3], [-1, 1]] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[[18, 18, 2, 6], [6, 2]] tm.assert_frame_equal(result, expected) @@ -434,6 +442,7 @@ def test_iloc_getitem_frame(self): s = Series(index=lrange(1, 5)) result = df.iloc[s.index] with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) expected = df.ix[[2, 4, 6, 8]] tm.assert_frame_equal(result, expected) @@ -609,6 +618,7 @@ def test_iloc_mask(self): # UserWarnings from reindex of a boolean mask with catch_warnings(record=True): + simplefilter("ignore", UserWarning) result = dict() for idx in [None, 'index', 'locs']: mask = (df.nums > 2).values diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index f64c50699461f..33b7c1b8154c7 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -6,7 +6,7 @@ import pytest import weakref -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from datetime import datetime from pandas.core.dtypes.common import ( @@ -419,11 +419,13 @@ def test_setitem_list(self): # ix with a list df = DataFrame(index=[0, 1], columns=[0]) with catch_warnings(record=True): + simplefilter("ignore") df.ix[1, 0] = [1, 2, 3] df.ix[1, 0] = [1, 2] result = DataFrame(index=[0, 1], columns=[0]) with catch_warnings(record=True): + simplefilter("ignore") result.ix[1, 0] = [1, 2] tm.assert_frame_equal(result, df) @@ -447,11 +449,13 @@ def view(self): df = DataFrame(index=[0, 1], columns=[0]) with catch_warnings(record=True): + simplefilter("ignore") df.ix[1, 0] = TO(1) df.ix[1, 0] = TO(2) result = DataFrame(index=[0, 1], columns=[0]) with catch_warnings(record=True): + simplefilter("ignore") result.ix[1, 0] = TO(2) tm.assert_frame_equal(result, df) @@ -459,6 +463,7 @@ def view(self): # remains object dtype even after setting it back df = DataFrame(index=[0, 1], columns=[0]) with catch_warnings(record=True): + simplefilter("ignore") df.ix[1, 0] = TO(1) df.ix[1, 0] = np.nan result = DataFrame(index=[0, 1], columns=[0]) @@ -629,6 +634,7 @@ def test_mixed_index_not_contains(self, index, val): def test_index_type_coercion(self): with catch_warnings(record=True): + simplefilter("ignore") # GH 11836 # if we have an index type and set it with something that looks @@ -760,16 +766,20 @@ def run_tests(df, rhs, right): left = df.copy() with catch_warnings(record=True): + # XXX: finer-filter here. + simplefilter("ignore") left.ix[s, l] = rhs tm.assert_frame_equal(left, right) left = df.copy() with catch_warnings(record=True): + simplefilter("ignore") left.ix[i, j] = rhs tm.assert_frame_equal(left, right) left = df.copy() with catch_warnings(record=True): + simplefilter("ignore") left.ix[r, c] = rhs tm.assert_frame_equal(left, right) @@ -821,6 +831,7 @@ def test_slice_with_zero_step_raises(self): tm.assert_raises_regex(ValueError, 'slice step cannot be zero', lambda: s.loc[::0]) with catch_warnings(record=True): + simplefilter("ignore") tm.assert_raises_regex(ValueError, 'slice step cannot be zero', lambda: s.ix[::0]) @@ -839,11 +850,13 @@ def test_indexing_dtypes_on_empty(self): # Check that .iloc and .ix return correct dtypes GH9983 df = DataFrame({'a': [1, 2, 3], 'b': ['b', 'b2', 'b3']}) with catch_warnings(record=True): + simplefilter("ignore") df2 = df.ix[[], :] assert df2.loc[:, 'a'].dtype == np.int64 tm.assert_series_equal(df2.loc[:, 'a'], df2.iloc[:, 0]) with catch_warnings(record=True): + simplefilter("ignore") tm.assert_series_equal(df2.loc[:, 'a'], df2.ix[:, 0]) def test_range_in_series_indexing(self): @@ -917,6 +930,7 @@ def test_no_reference_cycle(self): for name in ('loc', 'iloc', 'at', 'iat'): getattr(df, name) with catch_warnings(record=True): + simplefilter("ignore") getattr(df, 'ix') wr = weakref.ref(df) del df diff --git a/pandas/tests/indexing/test_indexing_slow.py b/pandas/tests/indexing/test_indexing_slow.py index f4d581f450363..61e5fdd7b9562 100644 --- a/pandas/tests/indexing/test_indexing_slow.py +++ b/pandas/tests/indexing/test_indexing_slow.py @@ -12,6 +12,7 @@ class TestIndexingSlow(object): @pytest.mark.slow + @pytest.mark.filterwarnings("ignore::pandas.errors.PerformanceWarning") def test_multiindex_get_loc(self): # GH7724, GH2646 with warnings.catch_warnings(record=True): diff --git a/pandas/tests/indexing/test_ix.py b/pandas/tests/indexing/test_ix.py index c84576c984525..04d0e04b5651e 100644 --- a/pandas/tests/indexing/test_ix.py +++ b/pandas/tests/indexing/test_ix.py @@ -14,15 +14,17 @@ from pandas.errors import PerformanceWarning -class TestIX(object): +def test_ix_deprecation(): + # GH 15114 + + df = DataFrame({'A': [1, 2, 3]}) + with tm.assert_produces_warning(DeprecationWarning, + check_stacklevel=False): + df.ix[1, 'A'] - def test_ix_deprecation(self): - # GH 15114 - df = DataFrame({'A': [1, 2, 3]}) - with tm.assert_produces_warning(DeprecationWarning, - check_stacklevel=False): - df.ix[1, 'A'] +@pytest.mark.filterwarnings("ignore:\\n.ix:DeprecationWarning") +class TestIX(object): def test_ix_loc_setitem_consistency(self): diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 2e52154d7679b..9fa705f923c88 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -3,7 +3,7 @@ import itertools import pytest -from warnings import catch_warnings +from warnings import catch_warnings, filterwarnings import numpy as np import pandas as pd @@ -699,6 +699,7 @@ def test_loc_name(self): assert result == 'index_name' with catch_warnings(record=True): + filterwarnings("ignore", "\\n.ix", DeprecationWarning) result = df.ix[[0, 1]].index.name assert result == 'index_name' diff --git a/pandas/tests/indexing/test_multiindex.py b/pandas/tests/indexing/test_multiindex.py index d2c4c8f5e149b..9e66dfad3ddc7 100644 --- a/pandas/tests/indexing/test_multiindex.py +++ b/pandas/tests/indexing/test_multiindex.py @@ -9,6 +9,7 @@ from pandas.tests.indexing.common import _mklbl +@pytest.mark.filterwarnings("ignore:\\n.ix:DeprecationWarning") class TestMultiIndexBasic(object): def test_iloc_getitem_multiindex2(self): @@ -1232,101 +1233,99 @@ def f(): tm.assert_frame_equal(df, expected) +@pytest.mark.filterwarnings('ignore:\\nPanel:FutureWarning') class TestMultiIndexPanel(object): def test_iloc_getitem_panel_multiindex(self): - with catch_warnings(record=True): + # GH 7199 + # Panel with multi-index + multi_index = MultiIndex.from_tuples([('ONE', 'one'), + ('TWO', 'two'), + ('THREE', 'three')], + names=['UPPER', 'lower']) + + simple_index = [x[0] for x in multi_index] + wd1 = Panel(items=['First', 'Second'], + major_axis=['a', 'b', 'c', 'd'], + minor_axis=multi_index) + + wd2 = Panel(items=['First', 'Second'], + major_axis=['a', 'b', 'c', 'd'], + minor_axis=simple_index) + + expected1 = wd1['First'].iloc[[True, True, True, False], [0, 2]] + result1 = wd1.iloc[0, [True, True, True, False], [0, 2]] # WRONG + tm.assert_frame_equal(result1, expected1) + + expected2 = wd2['First'].iloc[[True, True, True, False], [0, 2]] + result2 = wd2.iloc[0, [True, True, True, False], [0, 2]] + tm.assert_frame_equal(result2, expected2) + + expected1 = DataFrame(index=['a'], columns=multi_index, + dtype='float64') + result1 = wd1.iloc[0, [0], [0, 1, 2]] + tm.assert_frame_equal(result1, expected1) + + expected2 = DataFrame(index=['a'], columns=simple_index, + dtype='float64') + result2 = wd2.iloc[0, [0], [0, 1, 2]] + tm.assert_frame_equal(result2, expected2) + + # GH 7516 + mi = MultiIndex.from_tuples([(0, 'x'), (1, 'y'), (2, 'z')]) + p = Panel(np.arange(3 * 3 * 3, dtype='int64').reshape(3, 3, 3), + items=['a', 'b', 'c'], major_axis=mi, + minor_axis=['u', 'v', 'w']) + result = p.iloc[:, 1, 0] + expected = Series([3, 12, 21], index=['a', 'b', 'c'], name='u') + tm.assert_series_equal(result, expected) - # GH 7199 - # Panel with multi-index - multi_index = MultiIndex.from_tuples([('ONE', 'one'), - ('TWO', 'two'), - ('THREE', 'three')], - names=['UPPER', 'lower']) - - simple_index = [x[0] for x in multi_index] - wd1 = Panel(items=['First', 'Second'], - major_axis=['a', 'b', 'c', 'd'], - minor_axis=multi_index) - - wd2 = Panel(items=['First', 'Second'], - major_axis=['a', 'b', 'c', 'd'], - minor_axis=simple_index) - - expected1 = wd1['First'].iloc[[True, True, True, False], [0, 2]] - result1 = wd1.iloc[0, [True, True, True, False], [0, 2]] # WRONG - tm.assert_frame_equal(result1, expected1) - - expected2 = wd2['First'].iloc[[True, True, True, False], [0, 2]] - result2 = wd2.iloc[0, [True, True, True, False], [0, 2]] - tm.assert_frame_equal(result2, expected2) - - expected1 = DataFrame(index=['a'], columns=multi_index, - dtype='float64') - result1 = wd1.iloc[0, [0], [0, 1, 2]] - tm.assert_frame_equal(result1, expected1) - - expected2 = DataFrame(index=['a'], columns=simple_index, - dtype='float64') - result2 = wd2.iloc[0, [0], [0, 1, 2]] - tm.assert_frame_equal(result2, expected2) - - # GH 7516 - mi = MultiIndex.from_tuples([(0, 'x'), (1, 'y'), (2, 'z')]) - p = Panel(np.arange(3 * 3 * 3, dtype='int64').reshape(3, 3, 3), - items=['a', 'b', 'c'], major_axis=mi, - minor_axis=['u', 'v', 'w']) - result = p.iloc[:, 1, 0] - expected = Series([3, 12, 21], index=['a', 'b', 'c'], name='u') - tm.assert_series_equal(result, expected) - - result = p.loc[:, (1, 'y'), 'u'] - tm.assert_series_equal(result, expected) + result = p.loc[:, (1, 'y'), 'u'] + tm.assert_series_equal(result, expected) def test_panel_setitem_with_multiindex(self): - with catch_warnings(record=True): - # 10360 - # failing with a multi-index - arr = np.array([[[1, 2, 3], [0, 0, 0]], - [[0, 0, 0], [0, 0, 0]]], - dtype=np.float64) - - # reg index - axes = dict(items=['A', 'B'], major_axis=[0, 1], - minor_axis=['X', 'Y', 'Z']) - p1 = Panel(0., **axes) - p1.iloc[0, 0, :] = [1, 2, 3] - expected = Panel(arr, **axes) - tm.assert_panel_equal(p1, expected) - - # multi-indexes - axes['items'] = MultiIndex.from_tuples( - [('A', 'a'), ('B', 'b')]) - p2 = Panel(0., **axes) - p2.iloc[0, 0, :] = [1, 2, 3] - expected = Panel(arr, **axes) - tm.assert_panel_equal(p2, expected) - - axes['major_axis'] = MultiIndex.from_tuples( - [('A', 1), ('A', 2)]) - p3 = Panel(0., **axes) - p3.iloc[0, 0, :] = [1, 2, 3] - expected = Panel(arr, **axes) - tm.assert_panel_equal(p3, expected) - - axes['minor_axis'] = MultiIndex.from_product( - [['X'], range(3)]) - p4 = Panel(0., **axes) - p4.iloc[0, 0, :] = [1, 2, 3] - expected = Panel(arr, **axes) - tm.assert_panel_equal(p4, expected) - - arr = np.array( - [[[1, 0, 0], [2, 0, 0]], [[0, 0, 0], [0, 0, 0]]], - dtype=np.float64) - p5 = Panel(0., **axes) - p5.iloc[0, :, 0] = [1, 2] - expected = Panel(arr, **axes) - tm.assert_panel_equal(p5, expected) + # 10360 + # failing with a multi-index + arr = np.array([[[1, 2, 3], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0]]], + dtype=np.float64) + + # reg index + axes = dict(items=['A', 'B'], major_axis=[0, 1], + minor_axis=['X', 'Y', 'Z']) + p1 = Panel(0., **axes) + p1.iloc[0, 0, :] = [1, 2, 3] + expected = Panel(arr, **axes) + tm.assert_panel_equal(p1, expected) + + # multi-indexes + axes['items'] = MultiIndex.from_tuples( + [('A', 'a'), ('B', 'b')]) + p2 = Panel(0., **axes) + p2.iloc[0, 0, :] = [1, 2, 3] + expected = Panel(arr, **axes) + tm.assert_panel_equal(p2, expected) + + axes['major_axis'] = MultiIndex.from_tuples( + [('A', 1), ('A', 2)]) + p3 = Panel(0., **axes) + p3.iloc[0, 0, :] = [1, 2, 3] + expected = Panel(arr, **axes) + tm.assert_panel_equal(p3, expected) + + axes['minor_axis'] = MultiIndex.from_product( + [['X'], range(3)]) + p4 = Panel(0., **axes) + p4.iloc[0, 0, :] = [1, 2, 3] + expected = Panel(arr, **axes) + tm.assert_panel_equal(p4, expected) + + arr = np.array( + [[[1, 0, 0], [2, 0, 0]], [[0, 0, 0], [0, 0, 0]]], + dtype=np.float64) + p5 = Panel(0., **axes) + p5.iloc[0, :, 0] = [1, 2] + expected = Panel(arr, **axes) + tm.assert_panel_equal(p5, expected) diff --git a/pandas/tests/indexing/test_panel.py b/pandas/tests/indexing/test_panel.py index 1085e2a61be48..2cd05b5779f30 100644 --- a/pandas/tests/indexing/test_panel.py +++ b/pandas/tests/indexing/test_panel.py @@ -6,6 +6,7 @@ from pandas import Panel, date_range, DataFrame +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestPanel(object): def test_iloc_getitem_panel(self): @@ -110,6 +111,7 @@ def test_iloc_panel_issue(self): assert p.iloc[1, :3, 1].shape == (3, ) assert p.iloc[:3, 1, 1].shape == (3, ) + @pytest.mark.filterwarnings("ignore:\\n.ix:DeprecationWarning") def test_panel_getitem(self): with catch_warnings(record=True): diff --git a/pandas/tests/indexing/test_partial.py b/pandas/tests/indexing/test_partial.py index 3c7a7f070805d..5910f462cb3df 100644 --- a/pandas/tests/indexing/test_partial.py +++ b/pandas/tests/indexing/test_partial.py @@ -16,6 +16,8 @@ class TestPartialSetting(object): + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") + @pytest.mark.filterwarnings("ignore:\\n.ix:DeprecationWarning") def test_partial_setting(self): # GH2578, allow ix and friends to partially set @@ -404,6 +406,7 @@ def test_series_partial_set_with_name(self): result = ser.iloc[[1, 1, 0, 0]] tm.assert_series_equal(result, expected, check_index_type=True) + @pytest.mark.filterwarnings("ignore:\\n.ix") def test_partial_set_invalid(self): # GH 4940 diff --git a/pandas/tests/internals/test_internals.py b/pandas/tests/internals/test_internals.py index 34f22513106ba..86251ad7529d5 100644 --- a/pandas/tests/internals/test_internals.py +++ b/pandas/tests/internals/test_internals.py @@ -1285,7 +1285,7 @@ def test_deprecated_fastpath(): def test_validate_ndim(): values = np.array([1.0, 2.0]) placement = slice(2) - msg = "Wrong number of dimensions. values.ndim != ndim \[1 != 2\]" + msg = r"Wrong number of dimensions. values.ndim != ndim \[1 != 2\]" with tm.assert_raises_regex(ValueError, msg): make_block(values, placement, ndim=2) diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py index 9fc16c43f5c1d..7d54f93c9831e 100644 --- a/pandas/tests/io/formats/test_to_excel.py +++ b/pandas/tests/io/formats/test_to_excel.py @@ -6,8 +6,8 @@ import pytest import pandas.util.testing as tm -from warnings import catch_warnings from pandas.io.formats.excel import CSSToExcelConverter +from pandas.io.formats.css import CSSWarning @pytest.mark.parametrize('css,expected', [ @@ -272,6 +272,6 @@ def test_css_to_excel_bad_colors(input_color): "patternType": "solid" } - with catch_warnings(record=True): + with tm.assert_produces_warning(CSSWarning): convert = CSSToExcelConverter() assert expected == convert(css) diff --git a/pandas/tests/io/generate_legacy_storage_files.py b/pandas/tests/io/generate_legacy_storage_files.py index aa020ba4c0623..4ebf435f7d75f 100755 --- a/pandas/tests/io/generate_legacy_storage_files.py +++ b/pandas/tests/io/generate_legacy_storage_files.py @@ -35,7 +35,7 @@ """ from __future__ import print_function -from warnings import catch_warnings +from warnings import catch_warnings, filterwarnings from distutils.version import LooseVersion from pandas import (Series, DataFrame, Panel, SparseSeries, SparseDataFrame, @@ -187,6 +187,7 @@ def create_data(): ) with catch_warnings(record=True): + filterwarnings("ignore", "\\nPanel", FutureWarning) mixed_dup_panel = Panel({u'ItemA': frame[u'float'], u'ItemB': frame[u'int']}) mixed_dup_panel.items = [u'ItemA', u'ItemA'] diff --git a/pandas/tests/io/parser/compression.py b/pandas/tests/io/parser/compression.py index e4950af19ea95..5a28b6263f20f 100644 --- a/pandas/tests/io/parser/compression.py +++ b/pandas/tests/io/parser/compression.py @@ -30,9 +30,8 @@ def test_zip(self): 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() + with zipfile.ZipFile(path, mode='w') as tmp: + tmp.writestr('test_file', data) result = self.read_csv(path, compression='zip') tm.assert_frame_equal(result, expected) @@ -47,10 +46,9 @@ def test_zip(self): 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() + with zipfile.ZipFile(path, mode='w') as tmp: + for file_name in inner_file_names: + tmp.writestr(file_name, data) tm.assert_raises_regex(ValueError, 'Multiple files', self.read_csv, path, compression='zip') @@ -60,8 +58,8 @@ def test_zip(self): compression='infer') with tm.ensure_clean() as path: - tmp = zipfile.ZipFile(path, mode='w') - tmp.close() + with zipfile.ZipFile(path, mode='w') as tmp: + pass tm.assert_raises_regex(ValueError, 'Zero files', self.read_csv, path, compression='zip') @@ -84,9 +82,8 @@ def test_other_compression(self, compress_type, compress_method, ext): expected = self.read_csv(self.csv1) with tm.ensure_clean() as path: - tmp = compress_method(path, mode='wb') - tmp.write(data) - tmp.close() + with compress_method(path, mode='wb') as tmp: + tmp.write(data) result = self.read_csv(path, compression=compress_type) tm.assert_frame_equal(result, expected) @@ -100,9 +97,8 @@ def test_other_compression(self, compress_type, compress_method, ext): tm.assert_frame_equal(result, expected) with tm.ensure_clean('test.{}'.format(ext)) as path: - tmp = compress_method(path, mode='wb') - tmp.write(data) - tmp.close() + with compress_method(path, mode='wb') as tmp: + tmp.write(data) result = self.read_csv(path, compression='infer') tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/sas/test_sas7bdat.py b/pandas/tests/io/sas/test_sas7bdat.py index f4b14241ed80e..705387188438f 100644 --- a/pandas/tests/io/sas/test_sas7bdat.py +++ b/pandas/tests/io/sas/test_sas7bdat.py @@ -9,6 +9,8 @@ import pytest +# https://github.com/cython/cython/issues/1720 +@pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") class TestSAS7BDAT(object): @pytest.fixture(autouse=True) diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 991b8ee508760..73e29e6eb9a6a 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -44,6 +44,8 @@ def __fspath__(self): HERE = os.path.abspath(os.path.dirname(__file__)) +# 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 diff --git a/pandas/tests/io/test_compression.py b/pandas/tests/io/test_compression.py index 1806ddd2bbcc6..b62a1e6c4933e 100644 --- a/pandas/tests/io/test_compression.py +++ b/pandas/tests/io/test_compression.py @@ -1,5 +1,6 @@ import os import warnings +import contextlib import pytest @@ -8,12 +9,15 @@ import pandas.util.testing as tm +@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. - return warnings.catch_warnings(record=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", FutureWarning) + yield @pytest.mark.parametrize('obj', [ diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index 6741645e466f3..a639556eb07d6 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -611,6 +611,8 @@ def test_read_from_s3_url(self, ext): tm.assert_frame_equal(url_table, local_table) @pytest.mark.slow + # ignore warning from old xlrd + @pytest.mark.filterwarnings("ignore:This metho:PendingDeprecationWarning") def test_read_from_file_url(self, ext): # FILE @@ -2189,6 +2191,7 @@ def test_ExcelWriter_dispatch_raises(self): with tm.assert_raises_regex(ValueError, '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 = [] diff --git a/pandas/tests/io/test_packers.py b/pandas/tests/io/test_packers.py index 412e218f95c6f..ee45f8828d85e 100644 --- a/pandas/tests/io/test_packers.py +++ b/pandas/tests/io/test_packers.py @@ -91,6 +91,7 @@ def check_arbitrary(a, b): assert(a == b) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestPackers(object): def setup_method(self, method): @@ -105,6 +106,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): @@ -464,6 +466,7 @@ def test_basic(self): assert_categorical_equal(i, i_rec) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestNDFrame(TestPackers): def setup_method(self, method): @@ -486,10 +489,9 @@ def setup_method(self, method): 'int': DataFrame(dict(A=data['B'], B=Series(data['B']) + 1)), 'mixed': DataFrame(data)} - with catch_warnings(record=True): - self.panel = { - 'float': Panel(dict(ItemA=self.frame['float'], - ItemB=self.frame['float'] + 1))} + self.panel = { + 'float': Panel(dict(ItemA=self.frame['float'], + ItemB=self.frame['float'] + 1))} def test_basic_frame(self): @@ -846,6 +848,7 @@ def legacy_packer(request, datapath): return datapath(request.param) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestMsgpack(object): """ How to add msgpack tests: diff --git a/pandas/tests/io/test_pickle.py b/pandas/tests/io/test_pickle.py index 77b4a3c7cac5f..a47c3c01fc80e 100644 --- a/pandas/tests/io/test_pickle.py +++ b/pandas/tests/io/test_pickle.py @@ -14,7 +14,7 @@ """ import glob import pytest -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import os from distutils.version import LooseVersion @@ -202,6 +202,7 @@ def test_pickles(current_pickle_data, legacy_pickle): version = os.path.basename(os.path.dirname(legacy_pickle)) with catch_warnings(record=True): + simplefilter("ignore") compare(current_pickle_data, legacy_pickle, version) @@ -332,9 +333,9 @@ def compress_file(self, src_path, dest_path, compression): f = bz2.BZ2File(dest_path, "w") elif compression == 'zip': import zipfile - f = zipfile.ZipFile(dest_path, "w", - compression=zipfile.ZIP_DEFLATED) - f.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() f = lzma.LZMAFile(dest_path, "w") @@ -343,9 +344,8 @@ def compress_file(self, src_path, dest_path, compression): raise ValueError(msg) if compression != "zip": - with open(src_path, "rb") as fh: + with open(src_path, "rb") as fh, f: f.write(fh.read()) - f.close() def test_write_explicit(self, compression, get_random_path): base = get_random_path diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py index ddcfcc0842d1a..ea5f1684c0695 100644 --- a/pandas/tests/io/test_pytables.py +++ b/pandas/tests/io/test_pytables.py @@ -2,7 +2,7 @@ import os import tempfile from contextlib import contextmanager -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from distutils.version import LooseVersion import datetime @@ -40,6 +40,10 @@ LooseVersion('2.2') else 'zlib') +ignore_natural_naming_warning = pytest.mark.filterwarnings( + "ignore:object name:tables.exceptions.NaturalNameWarning" +) + # contextmanager to ensure the file cleanup @@ -139,12 +143,14 @@ def teardown_method(self, method): @pytest.mark.single +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestHDFStore(Base): def test_factory_fun(self): path = create_tempfile(self.path) try: - with catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): with get_store(path) as tbl: raise ValueError('blah') except ValueError: @@ -153,11 +159,13 @@ def test_factory_fun(self): safe_remove(path) try: - with catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): with get_store(path) as tbl: tbl['a'] = tm.makeDataFrame() - with catch_warnings(record=True): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): with get_store(path) as tbl: assert len(tbl) == 1 assert type(tbl['a']) == DataFrame @@ -425,8 +433,8 @@ 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 @@ -446,6 +454,7 @@ def test_repr(self): repr(s) str(s) + @ignore_natural_naming_warning def test_contains(self): with ensure_clean_store(self.path) as store: @@ -912,11 +921,15 @@ def test_put_mixed_type(self): # 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: @@ -1075,6 +1088,7 @@ def check(format, index): # PerformanceWarning with catch_warnings(record=True): + simplefilter("ignore", pd.errors.PerformanceWarning) check('fixed', index) @pytest.mark.skipif(not is_platform_little_endian(), @@ -1355,6 +1369,7 @@ def test_append_with_strings(self): with ensure_clean_store(self.path) as store: with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) wp = tm.makePanel() wp2 = wp.rename_axis( {x: "%s_extra" % x for x in wp.minor_axis}, axis=2) @@ -2553,6 +2568,7 @@ def test_terms(self): with ensure_clean_store(self.path) as store: with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) wp = tm.makePanel() wpneg = Panel.fromDict({-1: tm.makeDataFrame(), @@ -2758,8 +2774,10 @@ 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) + @pytest.mark.filterwarnings("ignore::pandas.errors.PerformanceWarning") def test_index_types(self): with catch_warnings(record=True): @@ -2988,6 +3006,9 @@ def test_wide(self): wp = tm.makePanel() self._check_roundtrip(wp, assert_panel_equal) + @pytest.mark.filterwarnings( + "ignore:\\nduplicate:pandas.io.pytables.DuplicateWarning" + ) def test_select_with_dups(self): # single dtypes @@ -3047,6 +3068,9 @@ def test_select_with_dups(self): result = store.select('df', columns=['B', 'A']) assert_frame_equal(result, expected, by_blocks=True) + @pytest.mark.filterwarnings( + "ignore:\\nduplicate:pandas.io.pytables.DuplicateWarning" + ) def test_wide_table_dups(self): with ensure_clean_store(self.path) as store: with catch_warnings(record=True): @@ -3589,6 +3613,9 @@ def test_select_iterator_many_empty_frames(self): # should be [] 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 @@ -3631,6 +3658,9 @@ 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: @@ -4533,7 +4563,8 @@ def test_legacy_table_read(self, datapath): datapath('io', 'data', 'legacy_hdf', 'legacy_table.h5'), mode='r') as store: - with catch_warnings(record=True): + with catch_warnings(): + simplefilter("ignore", pd.io.pytables.IncompatibilityWarning) store.select('df1') store.select('df2') store.select('wp1') @@ -4665,6 +4696,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) @@ -4933,6 +4965,7 @@ def test_columns_multiindex_modified(self): df_loaded = read_hdf(path, 'df', columns=cols2load) # noqa 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 @@ -5277,6 +5310,7 @@ def test_complex_mixed_table(self): reread = read_hdf(path, 'df') assert_frame_equal(df, reread) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_complex_across_dimensions_fixed(self): with catch_warnings(record=True): complex128 = np.array( @@ -5294,6 +5328,7 @@ def test_complex_across_dimensions_fixed(self): reread = read_hdf(path, 'obj') comp(obj, reread) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") 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')) diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index e4df7043919ae..237cc2936919e 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -18,7 +18,6 @@ """ from __future__ import print_function -from warnings import catch_warnings import pytest import sqlite3 import csv @@ -582,11 +581,11 @@ 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) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_to_sql_panel(self): - with catch_warnings(record=True): - panel = tm.makePanel() - pytest.raises(NotImplementedError, sql.to_sql, panel, - 'test_panel', self.conn) + panel = tm.makePanel() + pytest.raises(NotImplementedError, sql.to_sql, panel, + 'test_panel', self.conn) def test_roundtrip(self): sql.to_sql(self.test_frame1, 'test_frame_roundtrip', diff --git a/pandas/tests/io/test_stata.py b/pandas/tests/io/test_stata.py index cfe47cae7e5e1..303d3a3d8dbe9 100644 --- a/pandas/tests/io/test_stata.py +++ b/pandas/tests/io/test_stata.py @@ -120,7 +120,7 @@ def test_read_empty_dta(self, version): 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: @@ -388,10 +388,8 @@ 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. - assert len(w) == 1 written_and_read_again = self.read_dta(path) tm.assert_frame_equal( @@ -871,6 +869,9 @@ def test_drop_column(self): read_stata(self.dta15_117, convert_dates=True, columns=columns) @pytest.mark.parametrize('version', [114, 117]) + @pytest.mark.filterwarnings( + "ignore:\\nStata value:pandas.io.stata.ValueLabelTypeMismatch" + ) def test_categorical_writing(self, version): original = DataFrame.from_records( [ @@ -901,12 +902,10 @@ def test_categorical_writing(self, version): expected.index.name = 'index' with tm.ensure_clean() as path: - with warnings.catch_warnings(record=True) as w: # noqa - # Silence warnings - original.to_stata(path, version=version) - written_and_read_again = self.read_dta(path) - res = written_and_read_again.set_index('index') - tm.assert_frame_equal(res, expected, check_categorical=False) + original.to_stata(path, version=version) + written_and_read_again = self.read_dta(path) + res = written_and_read_again.set_index('index') + tm.assert_frame_equal(res, expected, check_categorical=False) def test_categorical_warnings_and_errors(self): # Warning for non-string labels @@ -933,10 +932,9 @@ def test_categorical_warnings_and_errors(self): original = pd.concat([original[col].astype('category') for col in original], axis=1) - with warnings.catch_warnings(record=True) as w: + with tm.assert_produces_warning(pd.io.stata.ValueLabelTypeMismatch): original.to_stata(path) # should get a warning for mixed content - assert len(w) == 1 @pytest.mark.parametrize('version', [114, 117]) def test_categorical_with_stata_missing_values(self, version): @@ -1445,7 +1443,7 @@ def test_convert_strl_name_swap(self): columns=['long1' * 10, 'long', 1]) original.index.name = 'index' - with warnings.catch_warnings(record=True) as w: # noqa + with tm.assert_produces_warning(pd.io.stata.InvalidColumnName): with tm.ensure_clean() as path: original.to_stata(path, convert_strl=['long', 1], version=117) reread = self.read_dta(path) diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 772989231e9a7..cd297c356d60e 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -628,6 +628,7 @@ 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", UserWarning) df = DataFrame(np.random.rand(10, 4), index=list(string.ascii_letters[:10])) @@ -1574,7 +1575,11 @@ 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)] tm.assert_almost_equal(rects[-1].get_height(), 1.0) @@ -1850,7 +1855,7 @@ def test_line_colors(self): tm.close() - ax2 = df.plot(colors=custom_colors) + ax2 = df.plot(color=custom_colors) lines2 = ax2.get_lines() for l1, l2 in zip(ax.get_lines(), lines2): diff --git a/pandas/tests/plotting/test_hist_method.py b/pandas/tests/plotting/test_hist_method.py index 864d39eba29c5..2864877550bac 100644 --- a/pandas/tests/plotting/test_hist_method.py +++ b/pandas/tests/plotting/test_hist_method.py @@ -12,6 +12,7 @@ from numpy.random import randn from pandas.plotting._core import grouped_hist +from pandas.plotting._compat import _mpl_ge_2_2_0 from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works) @@ -193,7 +194,11 @@ 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)] tm.assert_almost_equal(rects[-1].get_height(), 1.0) @@ -279,9 +284,15 @@ def test_grouped_hist_legacy(self): # make sure kwargs to hist are handled xf, yf = 20, 18 xrot, yrot = 30, 40 - axes = grouped_hist(df.A, by=df.C, normed=True, cumulative=True, + + 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) + 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)] diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index e80443954a434..8c84b785c88e4 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -212,6 +212,8 @@ def test_parallel_coordinates(self, iris): with tm.assert_produces_warning(FutureWarning): parallel_coordinates(df, 'Name', colors=colors) + # 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 diff --git a/pandas/tests/reshape/merge/test_join.py b/pandas/tests/reshape/merge/test_join.py index 09f511886583c..e965ff7a78a39 100644 --- a/pandas/tests/reshape/merge/test_join.py +++ b/pandas/tests/reshape/merge/test_join.py @@ -19,6 +19,7 @@ a_ = np.array +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestJoin(object): def setup_method(self, method): diff --git a/pandas/tests/reshape/test_concat.py b/pandas/tests/reshape/test_concat.py index 762b04cc3bd4f..2aaa04d571e69 100644 --- a/pandas/tests/reshape/test_concat.py +++ b/pandas/tests/reshape/test_concat.py @@ -1,5 +1,6 @@ -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from itertools import combinations +from collections import deque import datetime as dt import dateutil @@ -13,6 +14,7 @@ read_csv, isna, Series, date_range, Index, Panel, MultiIndex, Timestamp, DatetimeIndex, Categorical) +from pandas.compat import Iterable from pandas.core.dtypes.dtypes import CategoricalDtype from pandas.util import testing as tm from pandas.util.testing import (assert_frame_equal, @@ -1465,6 +1467,7 @@ def test_concat_mixed_objs(self): # invalid concatente of mixed dims with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) panel = tm.makePanel() pytest.raises(ValueError, lambda: concat([panel, s1], axis=1)) @@ -1503,59 +1506,61 @@ def test_dtype_coerceion(self): result = concat([df.iloc[[0]], df.iloc[[1]]]) tm.assert_series_equal(result.dtypes, df.dtypes) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_panel_concat_other_axes(self): - with catch_warnings(record=True): - panel = tm.makePanel() + panel = tm.makePanel() - p1 = panel.iloc[:, :5, :] - p2 = panel.iloc[:, 5:, :] + p1 = panel.iloc[:, :5, :] + p2 = panel.iloc[:, 5:, :] - result = concat([p1, p2], axis=1) - tm.assert_panel_equal(result, panel) + result = concat([p1, p2], axis=1) + tm.assert_panel_equal(result, panel) - p1 = panel.iloc[:, :, :2] - p2 = panel.iloc[:, :, 2:] + p1 = panel.iloc[:, :, :2] + p2 = panel.iloc[:, :, 2:] - result = concat([p1, p2], axis=2) - tm.assert_panel_equal(result, panel) + 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' + # if things are a bit misbehaved + p1 = panel.iloc[:2, :, :2] + p2 = panel.iloc[:, :, 2:] + p1['ItemC'] = 'baz' - result = concat([p1, p2], axis=2) + 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) + expected = panel.copy() + expected['ItemC'] = expected['ItemC'].astype('O') + expected.loc['ItemC', :, :2] = 'baz' + tm.assert_panel_equal(result, expected) + @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): - with catch_warnings(record=True): - # #2257 - def make_panel(): - index = 5 - cols = 3 + # #2257 + def make_panel(): + index = 5 + cols = 3 - 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({"Item%s" % x: df() for x in ['A', 'B', 'C']}) + 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({"Item%s" % x: df() for x in ['A', 'B', 'C']}) - panel1 = make_panel() - panel2 = make_panel() + panel1 = make_panel() + panel2 = make_panel() - panel2 = panel2.rename_axis({x: "%s_1" % x - for x in panel2.major_axis}, - axis=1) + panel2 = panel2.rename_axis({x: "%s_1" % x + for x in panel2.major_axis}, + axis=1) - 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_axis(lambda x: '%s_1' % x, axis=1) + panel3 = panel3.rename_axis(lambda x: '%s_1' % x, axis=2) - # it works! - concat([panel1, panel3], axis=1, verify_integrity=True, sort=sort) + # it works! + concat([panel1, panel3], axis=1, verify_integrity=True, sort=sort) def test_concat_series(self): @@ -1722,8 +1727,6 @@ def test_concat_series_axis1_same_names_ignore_index(self): tm.assert_index_equal(result.columns, expected) def test_concat_iterables(self): - from collections import deque, Iterable - # GH8645 check concat works with tuples, list, generators, and weird # stuff like deque and custom iterables df1 = DataFrame([1, 2, 3]) @@ -2351,30 +2354,30 @@ def test_concat_datetime_timezone(self): tm.assert_frame_equal(result, expected) # GH 13783: Concat after resample - with catch_warnings(record=True): - result = pd.concat([df1.resample('H').mean(), - df2.resample('H').mean()]) - 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) + 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.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): - with catch_warnings(record=True): - # GH 13247 - dims = pdt().ndim - dfs = [pdt(np.array([1], dtype=dt, ndmin=dims)), - pdt(np.array([np.nan], dtype=dt, ndmin=dims)), - pdt(np.array([5], dtype=dt, ndmin=dims))] - x = pd.concat(dfs) - assert x.values.dtype == dt + # GH 13247 + dims = pdt().ndim + dfs = [pdt(np.array([1], dtype=dt, ndmin=dims)), + pdt(np.array([np.nan], dtype=dt, ndmin=dims)), + pdt(np.array([5], dtype=dt, ndmin=dims))] + x = pd.concat(dfs) + assert x.values.dtype == dt @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): with catch_warnings(record=True): dims = pdt().ndim diff --git a/pandas/tests/reshape/test_reshape.py b/pandas/tests/reshape/test_reshape.py index 3f4ccd7693a8f..ed9ad06a9b371 100644 --- a/pandas/tests/reshape/test_reshape.py +++ b/pandas/tests/reshape/test_reshape.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # pylint: disable-msg=W0612,E1101 -from warnings import catch_warnings import pytest from collections import OrderedDict @@ -501,12 +500,12 @@ def test_get_dummies_duplicate_columns(self, df): class TestCategoricalReshape(object): + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_reshaping_panel_categorical(self): - with catch_warnings(record=True): - p = tm.makePanel() - p['str'] = 'foo' - df = p.to_frame() + p = tm.makePanel() + p['str'] = 'foo' + df = p.to_frame() df['category'] = df['str'].astype('category') result = df['category'].unstack() diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py index bcea47f42056b..d1f022ef982c0 100644 --- a/pandas/tests/series/indexing/test_datetime.py +++ b/pandas/tests/series/indexing/test_datetime.py @@ -383,6 +383,8 @@ def test_getitem_setitem_periodindex(): 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) diff --git a/pandas/tests/series/indexing/test_indexing.py b/pandas/tests/series/indexing/test_indexing.py index 25bc394e312a0..aa4f58089a933 100644 --- a/pandas/tests/series/indexing/test_indexing.py +++ b/pandas/tests/series/indexing/test_indexing.py @@ -390,6 +390,8 @@ def test_setslice(test_data): assert sl.index.is_unique +# 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] with tm.assert_raises_regex(ValueError, 'tuple-index'): diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py index d5d9e5f4f14de..9acd6501c3825 100644 --- a/pandas/tests/series/test_analytics.py +++ b/pandas/tests/series/test_analytics.py @@ -1640,8 +1640,35 @@ def test_value_counts_categorical_not_ordered(self): tm.assert_series_equal(idx.value_counts(normalize=True), exp) +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', @@ -1661,6 +1688,12 @@ def s_main_dtypes(): 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] + + class TestMode(object): @pytest.mark.parametrize('dropna, expected', [ @@ -1864,12 +1897,10 @@ def test_error(self, r): with tm.assert_raises_regex(TypeError, msg): method(arg) - @pytest.mark.parametrize( - "s", - [v for k, v in s_main_dtypes().iteritems()]) - def test_nsmallest_nlargest(self, s): + 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]]) diff --git a/pandas/tests/series/test_api.py b/pandas/tests/series/test_api.py index da9b03e81994d..3b82242626c20 100644 --- a/pandas/tests/series/test_api.py +++ b/pandas/tests/series/test_api.py @@ -1,6 +1,7 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 from collections import OrderedDict +import warnings import pydoc import pytest @@ -728,8 +729,12 @@ def test_dt_accessor_api_for_categorical(self): 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) + 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) diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index 9faf47ace242d..4817f5bdccc29 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -957,6 +957,8 @@ def test_constructor_set(self): values = frozenset(values) pytest.raises(TypeError, 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} diff --git a/pandas/tests/series/test_dtypes.py b/pandas/tests/series/test_dtypes.py index dd1b623f0f7ff..7aecaf340a3e0 100644 --- a/pandas/tests/series/test_dtypes.py +++ b/pandas/tests/series/test_dtypes.py @@ -428,8 +428,10 @@ def test_astype_empty_constructor_equality(self, dtype): if dtype not in ('S', 'V'): # poor support (if any) currently with warnings.catch_warnings(record=True): - # Generic timestamp dtypes ('M' and 'm') are deprecated, - # but we test that already in series/test_constructors.py + if dtype in ('M', 'm'): + # Generic timestamp dtypes ('M' and 'm') are deprecated, + # but we test that already in series/test_constructors.py + warnings.simplefilter("ignore", FutureWarning) init_empty = Series([], dtype=dtype) as_type_empty = Series([]).astype(dtype) diff --git a/pandas/tests/sparse/frame/test_frame.py b/pandas/tests/sparse/frame/test_frame.py index 30938966b5d1a..5e5a341ca76d6 100644 --- a/pandas/tests/sparse/frame/test_frame.py +++ b/pandas/tests/sparse/frame/test_frame.py @@ -3,7 +3,6 @@ import operator import pytest -from warnings import catch_warnings from numpy import nan import numpy as np import pandas as pd @@ -971,27 +970,26 @@ def _check(frame, orig): _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): - with catch_warnings(record=True): + def _check(frame): + dense_frame = frame.to_dense() # noqa - def _check(frame): - dense_frame = frame.to_dense() # noqa + wp = Panel.from_dict({'foo': frame}) + from_dense_lp = wp.to_frame() - wp = Panel.from_dict({'foo': frame}) - from_dense_lp = wp.to_frame() + from_sparse_lp = spf.stack_sparse_frame(frame) - from_sparse_lp = spf.stack_sparse_frame(frame) + tm.assert_numpy_array_equal(from_dense_lp.values, + from_sparse_lp.values) - tm.assert_numpy_array_equal(from_dense_lp.values, - from_sparse_lp.values) + _check(float_frame) + _check(float_frame_int_kind) - _check(float_frame) - _check(float_frame_int_kind) - - # for now - pytest.raises(Exception, _check, float_frame_fill0) - pytest.raises(Exception, _check, float_frame_fill2) + # for now + pytest.raises(Exception, _check, float_frame_fill0) + pytest.raises(Exception, _check, float_frame_fill2) def test_transpose(self, float_frame, float_frame_int_kind, float_frame_dense, diff --git a/pandas/tests/sparse/frame/test_to_from_scipy.py b/pandas/tests/sparse/frame/test_to_from_scipy.py index aef49c84fc2ad..a7f64bbe9a49f 100644 --- a/pandas/tests/sparse/frame/test_to_from_scipy.py +++ b/pandas/tests/sparse/frame/test_to_from_scipy.py @@ -1,6 +1,5 @@ import pytest import numpy as np -from warnings import catch_warnings from pandas.util import testing as tm from pandas import SparseDataFrame, SparseSeries from distutils.version import LooseVersion @@ -12,12 +11,16 @@ 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 @@ -69,6 +72,8 @@ def test_from_to_scipy(spmatrix, index, columns, fill_value, dtype): @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 @@ -108,8 +113,7 @@ def test_from_to_scipy_object(spmatrix, fill_value): tm.assert_frame_equal(sdf_obj.to_dense(), expected.to_dense()) # Assert spmatrices equal - with catch_warnings(record=True): - assert dict(sdf.to_coo().todok()) == dict(spm.todok()) + assert dict(sdf.to_coo().todok()) == dict(spm.todok()) # Ensure dtype is preserved if possible res_dtype = object @@ -117,6 +121,7 @@ def test_from_to_scipy_object(spmatrix, fill_value): 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) @@ -135,6 +140,7 @@ def test_from_scipy_correct_ordering(spmatrix): 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) diff --git a/pandas/tests/sparse/series/test_series.py b/pandas/tests/sparse/series/test_series.py index 921c30234660f..5b50606bf37bd 100644 --- a/pandas/tests/sparse/series/test_series.py +++ b/pandas/tests/sparse/series/test_series.py @@ -1022,6 +1022,9 @@ def test_round_trip_preserve_multiindex_names(self): @td.skip_if_no_scipy +@pytest.mark.filterwarnings( + "ignore:the matrix subclass:PendingDeprecationWarning" +) class TestSparseSeriesScipyInteraction(object): # Issue 8048: add SparseSeries coo methods diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index 70973801d7cda..abcfa4b320b22 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -62,6 +62,8 @@ def test_oo_optimizable(): @tm.network +# Cython import warning +@pytest.mark.filterwarnings("ignore:can't:ImportWarning") def test_statsmodels(): statsmodels = import_module('statsmodels') # noqa @@ -71,6 +73,8 @@ def test_statsmodels(): 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 @@ -82,7 +86,9 @@ def test_scikit_learn(df): clf.predict(digits.data[-1:]) +# Cython import warning and traitlets @tm.network +@pytest.mark.filterwarnings("ignore") def test_seaborn(): seaborn = import_module('seaborn') @@ -104,6 +110,10 @@ def test_pandas_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 @@ -111,6 +121,8 @@ def test_geopandas(): 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 diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index 7f9cddf9859a5..76e003c463e7d 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import pytest -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import pandas # noqa import pandas as pd from pandas.errors import AbstractMethodError @@ -48,6 +48,7 @@ def test_error_rename(): pass with catch_warnings(record=True): + simplefilter("ignore") try: raise ParserError() except pd.parser.CParserError: diff --git a/pandas/tests/test_expressions.py b/pandas/tests/test_expressions.py index 468463d3eba5f..c101fd25ce5e5 100644 --- a/pandas/tests/test_expressions.py +++ b/pandas/tests/test_expressions.py @@ -2,7 +2,7 @@ from __future__ import print_function # pylint: disable-msg=W0612,E1101 -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import re import operator import pytest @@ -38,6 +38,7 @@ columns=list('ABCD'), dtype='int64') with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) _frame_panel = Panel(dict(ItemA=_frame.copy(), ItemB=(_frame.copy() + 3), ItemC=_frame.copy(), @@ -191,6 +192,7 @@ def test_integer_arithmetic_series(self): self.run_series(self.integer.iloc[:, 0], self.integer.iloc[:, 0]) @pytest.mark.slow + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_integer_panel(self): self.run_panel(_integer2_panel, np.random.randint(1, 100)) @@ -201,6 +203,7 @@ def test_float_arithmetic_series(self): self.run_series(self.frame2.iloc[:, 0], self.frame2.iloc[:, 0]) @pytest.mark.slow + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_float_panel(self): self.run_panel(_frame2_panel, np.random.randn() + 0.1, binary_comp=0.8) @@ -215,6 +218,7 @@ def test_mixed_arithmetic_series(self): self.run_series(self.mixed2[col], self.mixed2[col], binary_comp=4) @pytest.mark.slow + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_mixed_panel(self): self.run_panel(_mixed2_panel, np.random.randint(1, 100), binary_comp=-2) diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py index ecd0af9c13d34..1718c6beaef55 100644 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # pylint: disable-msg=W0612,E1101,W0141 -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import datetime import itertools import pytest @@ -194,6 +194,7 @@ 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) @@ -206,6 +207,7 @@ def test_reindex_preserve_levels(self): assert chunk.index is new_index with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) chunk = self.ymd.ix[new_index] assert chunk.index is new_index @@ -269,6 +271,7 @@ def test_series_getitem(self): tm.assert_series_equal(result, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = s.ix[[(2000, 3, 10), (2000, 3, 13)]] tm.assert_series_equal(result, expected) @@ -348,6 +351,7 @@ def test_frame_getitem_setitem_multislice(self): tm.assert_series_equal(df['value'], result) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[:, 'value'] tm.assert_series_equal(df['value'], result) @@ -423,6 +427,7 @@ def test_getitem_tuple_plus_slice(self): expected = idf.loc[0, 0] expected2 = idf.xs((0, 0)) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) expected3 = idf.ix[0, 0] tm.assert_series_equal(result, expected) @@ -684,6 +689,7 @@ def test_frame_setitem_ix(self): assert df.loc[('bar', 'two'), 1] == 7 with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) df = self.frame.copy() df.columns = lrange(3) df.ix[('bar', 'two'), 1] = 7 @@ -713,6 +719,7 @@ def test_getitem_partial_column_select(self): tm.assert_frame_equal(result, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) result = df.ix[('a', 'y'), [1, 0]] tm.assert_frame_equal(result, expected) @@ -1294,6 +1301,7 @@ def test_swaplevel(self): def test_swaplevel_panel(self): with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) panel = Panel({'ItemA': self.frame, 'ItemB': self.frame * 2}) expected = panel.copy() expected.major_axis = expected.major_axis.swaplevel(0, 1) diff --git a/pandas/tests/test_nanops.py b/pandas/tests/test_nanops.py index a70ee80aee180..b6c2c65fb6dce 100644 --- a/pandas/tests/test_nanops.py +++ b/pandas/tests/test_nanops.py @@ -359,6 +359,7 @@ def test_returned_dtype(self): 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') @@ -394,12 +395,14 @@ def _minmax_wrap(self, value, axis=None, func=None): def test_nanmin(self): 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): - with warnings.catch_warnings(record=True): + 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) @@ -417,6 +420,7 @@ def _argminmax_wrap(self, value, axis=None, func=None): def test_nanargmax(self): 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, @@ -424,6 +428,7 @@ def test_nanargmax(self): def test_nanargmin(self): 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) diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py index b968c52ce3dfd..51c779c6a97a3 100644 --- a/pandas/tests/test_panel.py +++ b/pandas/tests/test_panel.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # pylint: disable=W0612,E1101 -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from datetime import datetime import operator import pytest @@ -30,49 +30,47 @@ def make_test_panel(): with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) _panel = tm.makePanel() tm.add_nans(_panel) _panel = _panel.copy() return _panel +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class PanelTests(object): panel = None def test_pickle(self): - with catch_warnings(record=True): - unpickled = tm.round_trip_pickle(self.panel) - assert_frame_equal(unpickled['ItemA'], self.panel['ItemA']) + unpickled = tm.round_trip_pickle(self.panel) + assert_frame_equal(unpickled['ItemA'], self.panel['ItemA']) def test_rank(self): - with catch_warnings(record=True): - pytest.raises(NotImplementedError, lambda: self.panel.rank()) + pytest.raises(NotImplementedError, lambda: self.panel.rank()) def test_cumsum(self): - with catch_warnings(record=True): - cumsum = self.panel.cumsum() - assert_frame_equal(cumsum['ItemA'], self.panel['ItemA'].cumsum()) + cumsum = self.panel.cumsum() + assert_frame_equal(cumsum['ItemA'], self.panel['ItemA'].cumsum()) def not_hashable(self): - with catch_warnings(record=True): - c_empty = Panel() - c = Panel(Panel([[[1]]])) - pytest.raises(TypeError, hash, c_empty) - pytest.raises(TypeError, hash, c) + c_empty = Panel() + c = Panel(Panel([[[1]]])) + pytest.raises(TypeError, hash, c_empty) + pytest.raises(TypeError, hash, c) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class SafeForLongAndSparse(object): def test_repr(self): repr(self.panel) def test_copy_names(self): - with catch_warnings(record=True): - for attr in ('major_axis', 'minor_axis'): - getattr(self.panel, attr).name = None - cp = self.panel.copy() - getattr(cp, attr).name = 'foo' - assert getattr(self.panel, attr).name is None + for attr in ('major_axis', 'minor_axis'): + getattr(self.panel, attr).name = None + cp = self.panel.copy() + getattr(cp, attr).name = 'foo' + assert getattr(self.panel, attr).name is None def test_iter(self): tm.equalContents(list(self.panel), self.panel.items) @@ -91,6 +89,8 @@ def test_mean(self): def test_prod(self): self._check_stat_op('prod', np.prod, skipna_alternative=np.nanprod) + @pytest.mark.filterwarnings("ignore:Invalid value:RuntimeWarning") + @pytest.mark.filterwarnings("ignore:All-NaN:RuntimeWarning") def test_median(self): def wrapper(x): if isna(x).any(): @@ -99,13 +99,13 @@ def wrapper(x): self._check_stat_op('median', wrapper) + @pytest.mark.filterwarnings("ignore:Invalid value:RuntimeWarning") def test_min(self): - with catch_warnings(record=True): - self._check_stat_op('min', np.min) + self._check_stat_op('min', np.min) + @pytest.mark.filterwarnings("ignore:Invalid value:RuntimeWarning") def test_max(self): - with catch_warnings(record=True): - self._check_stat_op('max', np.max) + self._check_stat_op('max', np.max) @td.skip_if_no_scipy def test_skew(self): @@ -181,6 +181,7 @@ def wrapper(x): numeric_only=True) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class SafeForSparse(object): def test_get_axis(self): @@ -240,48 +241,46 @@ def test_get_plane_axes(self): index, columns = self.panel._get_plane_axes(0) def test_truncate(self): - with catch_warnings(record=True): - dates = self.panel.major_axis - start, end = dates[1], dates[5] + 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) + trunced = self.panel.truncate(start, end, axis='major') + expected = self.panel['ItemA'].truncate(start, end) - assert_frame_equal(trunced['ItemA'], expected) + assert_frame_equal(trunced['ItemA'], expected) - trunced = self.panel.truncate(before=start, axis='major') - expected = self.panel['ItemA'].truncate(before=start) + trunced = self.panel.truncate(before=start, axis='major') + expected = self.panel['ItemA'].truncate(before=start) - assert_frame_equal(trunced['ItemA'], expected) + assert_frame_equal(trunced['ItemA'], expected) - trunced = self.panel.truncate(after=end, axis='major') - expected = self.panel['ItemA'].truncate(after=end) + trunced = self.panel.truncate(after=end, axis='major') + expected = self.panel['ItemA'].truncate(after=end) - assert_frame_equal(trunced['ItemA'], expected) + assert_frame_equal(trunced['ItemA'], expected) def test_arith(self): - with catch_warnings(record=True): - 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 - - pytest.raises(Exception, self.panel.__add__, - self.panel['ItemA']) + 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 + + pytest.raises(Exception, self.panel.__add__, + self.panel['ItemA']) @staticmethod def _test_op(panel, op): @@ -300,100 +299,99 @@ def test_iteritems(self): assert len(list(self.panel.iteritems())) == len(self.panel.items) def test_combineFrame(self): - with catch_warnings(record=True): - def check_op(op, name): - # items - df = self.panel['ItemA'] + def check_op(op, name): + # items + df = self.panel['ItemA'] - func = getattr(self.panel, name) + func = getattr(self.panel, name) - result = func(df, axis='items') + result = func(df, axis='items') - assert_frame_equal( - result['ItemB'], op(self.panel['ItemB'], df)) + 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') + # major + xs = self.panel.major_xs(self.panel.major_axis[0]) + result = func(xs, axis='major') - idx = self.panel.major_axis[1] + idx = self.panel.major_axis[1] - assert_frame_equal(result.major_xs(idx), - op(self.panel.major_xs(idx), xs)) + 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') + # minor + xs = self.panel.minor_xs(self.panel.minor_axis[0]) + result = func(xs, axis='minor') - idx = self.panel.minor_axis[1] + idx = self.panel.minor_axis[1] - assert_frame_equal(result.minor_xs(idx), - op(self.panel.minor_xs(idx), xs)) + 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') + 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 + 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): - with catch_warnings(record=True): - result = self.panel.add(self.panel) - assert_panel_equal(result, self.panel * 2) + result = self.panel.add(self.panel) + assert_panel_equal(result, self.panel * 2) def test_neg(self): - with catch_warnings(record=True): - assert_panel_equal(-self.panel, self.panel * -1) + assert_panel_equal(-self.panel, self.panel * -1) # issue 7692 def test_raise_when_not_implemented(self): - with catch_warnings(record=True): - p = Panel(np.arange(3 * 4 * 5).reshape(3, 4, 5), - items=['ItemA', 'ItemB', 'ItemC'], - major_axis=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 pytest.raises(NotImplementedError): - getattr(p, op)(d, axis=0) + p = Panel(np.arange(3 * 4 * 5).reshape(3, 4, 5), + items=['ItemA', 'ItemB', 'ItemC'], + major_axis=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 pytest.raises(NotImplementedError): + getattr(p, op)(d, axis=0) def test_select(self): - with catch_warnings(record=True): - p = self.panel + p = self.panel - # select items + # select items + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): result = p.select(lambda x: x in ('ItemA', 'ItemC'), axis='items') - expected = p.reindex(items=['ItemA', 'ItemC']) - assert_panel_equal(result, expected) + expected = p.reindex(items=['ItemA', 'ItemC']) + assert_panel_equal(result, expected) - # select major_axis + # select major_axis + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): 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) - assert_panel_equal(result, expected) + new_major = p.major_axis[p.major_axis >= datetime(2000, 1, 15)] + expected = p.reindex(major=new_major) + assert_panel_equal(result, expected) - # select minor_axis + # select minor_axis + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): result = p.select(lambda x: x in ('D', 'A'), axis=2) - expected = p.reindex(minor=['A', 'D']) - assert_panel_equal(result, expected) + expected = p.reindex(minor=['A', 'D']) + assert_panel_equal(result, expected) - # corner case, empty thing + # corner case, empty thing + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): result = p.select(lambda x: x in ('foo', ), axis='items') - assert_panel_equal(result, p.reindex(items=[])) + assert_panel_equal(result, p.reindex(items=[])) def test_get_value(self): for item in self.panel.items: @@ -407,211 +405,204 @@ def test_get_value(self): def test_abs(self): - with catch_warnings(record=True): - result = self.panel.abs() - result2 = abs(self.panel) - expected = np.abs(self.panel) - assert_panel_equal(result, expected) - assert_panel_equal(result2, expected) + result = self.panel.abs() + result2 = abs(self.panel) + expected = np.abs(self.panel) + assert_panel_equal(result, expected) + 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) - assert result.name == 'A' - assert result2.name == 'A' + 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) + assert result.name == 'A' + assert result2.name == 'A' +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class CheckIndexing(object): def test_getitem(self): pytest.raises(Exception, self.panel.__getitem__, 'ItemQ') def test_delitem_and_pop(self): - with catch_warnings(record=True): - expected = self.panel['ItemA'] - result = self.panel.pop('ItemA') - assert_frame_equal(expected, result) - assert 'ItemA' not in self.panel.items + expected = self.panel['ItemA'] + result = self.panel.pop('ItemA') + assert_frame_equal(expected, result) + assert 'ItemA' not in self.panel.items - del self.panel['ItemB'] - assert 'ItemB' not in self.panel.items - pytest.raises(Exception, self.panel.__delitem__, 'ItemB') + del self.panel['ItemB'] + assert 'ItemB' not in self.panel.items + pytest.raises(Exception, self.panel.__delitem__, 'ItemB') - values = np.empty((3, 3, 3)) - values[0] = 0 - values[1] = 1 - values[2] = 2 + values = np.empty((3, 3, 3)) + values[0] = 0 + values[1] = 1 + values[2] = 2 - panel = Panel(values, lrange(3), lrange(3), lrange(3)) + panel = Panel(values, lrange(3), lrange(3), lrange(3)) - # did we delete the right row? + # did we delete the right row? - panelc = panel.copy() - del panelc[0] - tm.assert_frame_equal(panelc[1], panel[1]) - tm.assert_frame_equal(panelc[2], panel[2]) + panelc = panel.copy() + del panelc[0] + tm.assert_frame_equal(panelc[1], panel[1]) + tm.assert_frame_equal(panelc[2], panel[2]) - panelc = panel.copy() - del panelc[1] - tm.assert_frame_equal(panelc[0], panel[0]) - tm.assert_frame_equal(panelc[2], panel[2]) + panelc = panel.copy() + del panelc[1] + tm.assert_frame_equal(panelc[0], panel[0]) + tm.assert_frame_equal(panelc[2], panel[2]) - panelc = panel.copy() - del panelc[2] - tm.assert_frame_equal(panelc[1], panel[1]) - tm.assert_frame_equal(panelc[0], panel[0]) + panelc = panel.copy() + del panelc[2] + tm.assert_frame_equal(panelc[1], panel[1]) + tm.assert_frame_equal(panelc[0], panel[0]) def test_setitem(self): - with catch_warnings(record=True): - lp = self.panel.filter(['ItemA', 'ItemB']).to_frame() - with pytest.raises(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 - assert self.panel['ItemG'].values.dtype == np.int64 - assert self.panel['ItemE'].values.dtype == np.bool_ - - # object dtype - self.panel['ItemQ'] = 'foo' - assert self.panel['ItemQ'].values.dtype == np.object_ - - # boolean dtype - self.panel['ItemP'] = self.panel['ItemA'] > 0 - assert self.panel['ItemP'].values.dtype == np.bool_ - - pytest.raises(TypeError, self.panel.__setitem__, 'foo', - self.panel.loc[['ItemP']]) - - # bad shape - p = Panel(np.random.randn(4, 3, 2)) - with tm.assert_raises_regex(ValueError, - r"shape of value must be " - r"\(3, 2\), shape of given " - r"object was \(4, 2\)"): - p[0] = np.random.randn(4, 2) + lp = self.panel.filter(['ItemA', 'ItemB']).to_frame() + with pytest.raises(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 + assert self.panel['ItemG'].values.dtype == np.int64 + assert self.panel['ItemE'].values.dtype == np.bool_ + + # object dtype + self.panel['ItemQ'] = 'foo' + assert self.panel['ItemQ'].values.dtype == np.object_ + + # boolean dtype + self.panel['ItemP'] = self.panel['ItemA'] > 0 + assert self.panel['ItemP'].values.dtype == np.bool_ + + pytest.raises(TypeError, self.panel.__setitem__, 'foo', + self.panel.loc[['ItemP']]) + + # bad shape + p = Panel(np.random.randn(4, 3, 2)) + with tm.assert_raises_regex(ValueError, + r"shape of value must be " + r"\(3, 2\), shape of given " + r"object was \(4, 2\)"): + p[0] = np.random.randn(4, 2) def test_setitem_ndarray(self): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - # 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 = notna(panel.iloc[:, :, 0]) - panel.loc[:, :, 'NewMinor'] = newminor - assert_frame_equal(panel.loc[:, :, 'NewMinor'], - newminor.astype(object)) - - newmajor = notna(panel.iloc[:, 0, :]) - panel.loc[:, 'NewMajor', :] = newmajor - assert_frame_equal(panel.loc[:, 'NewMajor', :], - newmajor.astype(object)) + # 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 = notna(panel.iloc[:, :, 0]) + panel.loc[:, :, 'NewMinor'] = newminor + assert_frame_equal(panel.loc[:, :, 'NewMinor'], + newminor.astype(object)) + + newmajor = notna(panel.iloc[:, 0, :]) + panel.loc[:, 'NewMajor', :] = newmajor + assert_frame_equal(panel.loc[:, 'NewMajor', :], + newmajor.astype(object)) def test_major_xs(self): - with catch_warnings(record=True): - ref = self.panel['ItemA'] + ref = self.panel['ItemA'] - idx = self.panel.major_axis[5] - xs = self.panel.major_xs(idx) + 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) - assert result.name == 'ItemA' + result = xs['ItemA'] + assert_series_equal(result, ref.xs(idx), check_names=False) + assert result.name == 'ItemA' - # not contained - idx = self.panel.major_axis[0] - BDay() - pytest.raises(Exception, self.panel.major_xs, idx) + # not contained + idx = self.panel.major_axis[0] - BDay() + pytest.raises(Exception, self.panel.major_xs, idx) def test_major_xs_mixed(self): - with catch_warnings(record=True): - self.panel['ItemD'] = 'foo' - xs = self.panel.major_xs(self.panel.major_axis[0]) - assert xs['ItemA'].dtype == np.float64 - assert xs['ItemD'].dtype == np.object_ + self.panel['ItemD'] = 'foo' + xs = self.panel.major_xs(self.panel.major_axis[0]) + assert xs['ItemA'].dtype == np.float64 + assert xs['ItemD'].dtype == np.object_ def test_minor_xs(self): - with catch_warnings(record=True): - ref = self.panel['ItemA'] + ref = self.panel['ItemA'] - idx = self.panel.minor_axis[1] - xs = self.panel.minor_xs(idx) + idx = self.panel.minor_axis[1] + xs = self.panel.minor_xs(idx) - assert_series_equal(xs['ItemA'], ref[idx], check_names=False) + assert_series_equal(xs['ItemA'], ref[idx], check_names=False) - # not contained - pytest.raises(Exception, self.panel.minor_xs, 'E') + # not contained + pytest.raises(Exception, self.panel.minor_xs, 'E') def test_minor_xs_mixed(self): - with catch_warnings(record=True): - self.panel['ItemD'] = 'foo' + self.panel['ItemD'] = 'foo' - xs = self.panel.minor_xs('D') - assert xs['ItemA'].dtype == np.float64 - assert xs['ItemD'].dtype == np.object_ + xs = self.panel.minor_xs('D') + assert xs['ItemA'].dtype == np.float64 + assert xs['ItemD'].dtype == np.object_ def test_xs(self): - with catch_warnings(record=True): - itemA = self.panel.xs('ItemA', axis=0) - expected = self.panel['ItemA'] - tm.assert_frame_equal(itemA, expected) + itemA = self.panel.xs('ItemA', axis=0) + expected = self.panel['ItemA'] + tm.assert_frame_equal(itemA, expected) - # Get a view by default. - itemA_view = self.panel.xs('ItemA', axis=0) - itemA_view.values[:] = np.nan + # Get a view by default. + itemA_view = self.panel.xs('ItemA', axis=0) + itemA_view.values[:] = np.nan - assert np.isnan(self.panel['ItemA'].values).all() + assert np.isnan(self.panel['ItemA'].values).all() - # Mixed-type yields a copy. - self.panel['strings'] = 'foo' - result = self.panel.xs('D', axis=2) - assert result._is_copy is not None + # Mixed-type yields a copy. + self.panel['strings'] = 'foo' + result = self.panel.xs('D', axis=2) + assert result._is_copy is not None def test_getitem_fancy_labels(self): - with catch_warnings(record=True): - p = self.panel + p = self.panel - items = p.items[[1, 0]] - dates = p.major_axis[::2] - cols = ['D', 'C', 'F'] + items = p.items[[1, 0]] + dates = p.major_axis[::2] + cols = ['D', 'C', 'F'] - # all 3 specified + # all 3 specified + with catch_warnings(): + simplefilter("ignore", FutureWarning) + # XXX: warning in _validate_read_indexer assert_panel_equal(p.loc[items, dates, cols], p.reindex(items=items, major=dates, minor=cols)) @@ -670,132 +661,127 @@ def test_getitem_fancy_xs(self): assert_series_equal(p.loc[:, date, col], p.major_xs(date).loc[col]) def test_getitem_fancy_xs_check_view(self): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - p = self.panel - # GH 12533 + 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']]) + 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): - with catch_warnings(record=True): - 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 = 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 + a.loc[:, 22, [111, 333]] = b - assert_frame_equal(a.loc[:, 22, [111, 333]], b) + assert_frame_equal(a.loc[:, 22, [111, 333]], b) def test_ix_align(self): - with catch_warnings(record=True): - 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() + 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.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(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) + 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): - with catch_warnings(record=True): - 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) + 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() @@ -805,83 +791,85 @@ def _check_view(self, indexer, comp): comp(cp.loc[indexer].reindex_like(obj), obj) def test_logical_with_nas(self): - with catch_warnings(record=True): - d = Panel({'ItemA': {'a': [np.nan, False]}, - 'ItemB': {'a': [True, True]}}) + 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) + 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) + # 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): - with catch_warnings(record=True): - assert_panel_equal(-self.panel, -1 * self.panel) + assert_panel_equal(-self.panel, -1 * self.panel) def test_invert(self): - with catch_warnings(record=True): - assert_panel_equal(-(self.panel < 0), ~(self.panel < 0)) + assert_panel_equal(-(self.panel < 0), ~(self.panel < 0)) def test_comparisons(self): - with catch_warnings(record=True): - p1 = tm.makePanel() - p2 = tm.makePanel() + p1 = tm.makePanel() + p2 = tm.makePanel() - tp = p1.reindex(items=p1.items + ['foo']) - df = p1[p1.items[0]] + tp = p1.reindex(items=p1.items + ['foo']) + df = p1[p1.items[0]] - def test_comp(func): + def test_comp(func): - # versus same index - result = func(p1, p2) - tm.assert_numpy_array_equal(result.values, - func(p1.values, p2.values)) + # versus same index + result = func(p1, p2) + tm.assert_numpy_array_equal(result.values, + func(p1.values, p2.values)) - # versus non-indexed same objs - pytest.raises(Exception, func, p1, tp) + # versus non-indexed same objs + pytest.raises(Exception, func, p1, tp) - # versus different objs - pytest.raises(Exception, func, p1, df) + # versus different objs + pytest.raises(Exception, func, p1, df) - # versus scalar - result3 = func(self.panel, 0) - tm.assert_numpy_array_equal(result3.values, - func(self.panel.values, 0)) + # versus scalar + result3 = func(self.panel, 0) + tm.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) + 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): - with catch_warnings(record=True): - for item in self.panel.items: - for mjr in self.panel.major_axis[::2]: - for mnr in self.panel.minor_axis: + for item in self.panel.items: + for mjr in self.panel.major_axis[::2]: + for mnr in self.panel.minor_axis: + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): result = self.panel.get_value(item, mjr, mnr) - expected = self.panel[item][mnr][mjr] - assert_almost_equal(result, expected) + expected = self.panel[item][mnr][mjr] + assert_almost_equal(result, expected) + with catch_warnings(): + simplefilter("ignore", FutureWarning) with tm.assert_raises_regex(TypeError, "There must be an argument " "for each axis"): self.panel.get_value('a') def test_set_value(self): - with catch_warnings(record=True): - for item in self.panel.items: - for mjr in self.panel.major_axis[::2]: - for mnr in self.panel.minor_axis: + for item in self.panel.items: + for mjr in self.panel.major_axis[::2]: + for mnr in self.panel.minor_axis: + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): self.panel.set_value(item, mjr, mnr, 1.) - tm.assert_almost_equal(self.panel[item][mnr][mjr], 1.) + tm.assert_almost_equal(self.panel[item][mnr][mjr], 1.) - # resize + # resize + with catch_warnings(): + simplefilter("ignore", FutureWarning) res = self.panel.set_value('ItemE', 'foo', 'bar', 1.5) assert isinstance(res, Panel) assert res is not self.panel @@ -896,6 +884,7 @@ def test_set_value(self): self.panel.set_value('a') +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestPanel(PanelTests, CheckIndexing, SafeForLongAndSparse, SafeForSparse): @@ -906,314 +895,298 @@ def setup_method(self, method): self.panel.items.name = None def test_constructor(self): - with catch_warnings(record=True): - # with BlockManager - wp = Panel(self.panel._data) - assert wp._data is self.panel._data - - wp = Panel(self.panel._data, copy=True) - assert wp._data is not self.panel._data - tm.assert_panel_equal(wp, self.panel) - - # strings handled prop - wp = Panel([[['foo', 'foo', 'foo', ], ['foo', 'foo', 'foo']]]) - assert wp.values.dtype == np.object_ - - vals = self.panel.values - - # no copy - wp = Panel(vals) - assert wp.values is vals - - # copy - wp = Panel(vals, copy=True) - assert wp.values is not 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) - - tm.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) - - tm.assert_panel_equal(wp, Panel(vals, dtype='float32')) + # with BlockManager + wp = Panel(self.panel._data) + assert wp._data is self.panel._data + + wp = Panel(self.panel._data, copy=True) + assert wp._data is not self.panel._data + tm.assert_panel_equal(wp, self.panel) + + # strings handled prop + wp = Panel([[['foo', 'foo', 'foo', ], ['foo', 'foo', 'foo']]]) + assert wp.values.dtype == np.object_ + + vals = self.panel.values + + # no copy + wp = Panel(vals) + assert wp.values is vals + + # copy + wp = Panel(vals, copy=True) + assert wp.values is not 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) + + tm.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) + + tm.assert_panel_equal(wp, Panel(vals, dtype='float32')) def test_constructor_cast(self): - with catch_warnings(record=True): - zero_filled = self.panel.fillna(0) + zero_filled = self.panel.fillna(0) - casted = Panel(zero_filled._data, dtype=int) - casted2 = Panel(zero_filled.values, dtype=int) + 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) + 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) + 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) + 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']]] - pytest.raises(ValueError, Panel, data, dtype=float) + # can't cast + data = [[['foo', 'bar', 'baz']]] + pytest.raises(ValueError, Panel, data, dtype=float) def test_constructor_empty_panel(self): - with catch_warnings(record=True): - empty = Panel() - assert len(empty.items) == 0 - assert len(empty.major_axis) == 0 - assert len(empty.minor_axis) == 0 + empty = Panel() + assert len(empty.items) == 0 + assert len(empty.major_axis) == 0 + assert len(empty.minor_axis) == 0 def test_constructor_observe_dtype(self): - with catch_warnings(record=True): - # GH #411 - panel = Panel(items=lrange(3), major_axis=lrange(3), - minor_axis=lrange(3), dtype='O') - assert panel.values.dtype == np.object_ + # GH #411 + panel = Panel(items=lrange(3), major_axis=lrange(3), + minor_axis=lrange(3), dtype='O') + assert panel.values.dtype == np.object_ def test_constructor_dtypes(self): - with catch_warnings(record=True): - # GH #797 - - def _check_dtype(panel, dtype): - for i in panel.items: - assert 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) + # GH #797 + + def _check_dtype(panel, dtype): + for i in panel.items: + assert 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 catch_warnings(record=True): - with tm.assert_raises_regex(ValueError, "The number of dimensions required is 3"): # noqa - Panel(np.random.randn(10, 2)) + with tm.assert_raises_regex(ValueError, "The number of dimensions required is 3"): # noqa + Panel(np.random.randn(10, 2)) def test_consolidate(self): - with catch_warnings(record=True): - assert self.panel._data.is_consolidated() + assert self.panel._data.is_consolidated() - self.panel['foo'] = 1. - assert not self.panel._data.is_consolidated() + self.panel['foo'] = 1. + assert not self.panel._data.is_consolidated() - panel = self.panel._consolidate() - assert panel._data.is_consolidated() + panel = self.panel._consolidate() + assert panel._data.is_consolidated() def test_ctor_dict(self): - with catch_warnings(record=True): - itema = self.panel['ItemA'] - itemb = self.panel['ItemB'] + 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)} + 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 + wp = Panel.from_dict(d) + wp2 = Panel.from_dict(d2) # nested Dict - # TODO: unused? - wp3 = Panel.from_dict(d3) # noqa + # TODO: unused? + wp3 = Panel.from_dict(d3) # noqa - tm.assert_index_equal(wp.major_axis, self.panel.major_axis) - assert_panel_equal(wp, wp2) + tm.assert_index_equal(wp.major_axis, self.panel.major_axis) + assert_panel_equal(wp, wp2) - # intersect - wp = Panel.from_dict(d, intersect=True) - tm.assert_index_equal(wp.major_axis, itemb.index[5:]) + # intersect + wp = Panel.from_dict(d, intersect=True) + tm.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)) + # 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} + # a pathological case + d4 = {'A': None, 'B': None} - # TODO: unused? - wp4 = Panel.from_dict(d4) # noqa + # TODO: unused? + wp4 = Panel.from_dict(d4) # noqa - assert_panel_equal(Panel(d4), Panel(items=['A', 'B'])) + assert_panel_equal(Panel(d4), Panel(items=['A', 'B'])) - # cast - dcasted = {k: v.reindex(wp.major_axis).fillna(0) - for k, v in compat.iteritems(d)} - result = Panel(dcasted, dtype=int) - expected = Panel({k: v.astype(int) - for k, v in compat.iteritems(dcasted)}) - assert_panel_equal(result, expected) + # cast + dcasted = {k: v.reindex(wp.major_axis).fillna(0) + for k, v in compat.iteritems(d)} + result = Panel(dcasted, dtype=int) + expected = Panel({k: v.astype(int) + for k, v in compat.iteritems(dcasted)}) + assert_panel_equal(result, expected) - result = Panel(dcasted, dtype=np.int32) - expected = Panel({k: v.astype(np.int32) - for k, v in compat.iteritems(dcasted)}) - assert_panel_equal(result, expected) + result = Panel(dcasted, dtype=np.int32) + expected = Panel({k: v.astype(np.int32) + for k, v in compat.iteritems(dcasted)}) + assert_panel_equal(result, expected) def test_constructor_dict_mixed(self): - with catch_warnings(record=True): - data = {k: v.values for k, v in self.panel.iteritems()} - result = Panel(data) - exp_major = Index(np.arange(len(self.panel.major_axis))) - tm.assert_index_equal(result.major_axis, exp_major) + data = {k: v.values for k, v in self.panel.iteritems()} + result = Panel(data) + exp_major = Index(np.arange(len(self.panel.major_axis))) + tm.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) + 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) + data['ItemC'] = self.panel['ItemC'] + result = Panel(data) + assert_panel_equal(result, self.panel) - # corner, blow up - data['ItemB'] = data['ItemB'][:-1] - pytest.raises(Exception, Panel, data) + # corner, blow up + data['ItemB'] = data['ItemB'][:-1] + pytest.raises(Exception, Panel, data) - data['ItemB'] = self.panel['ItemB'].values[:, :-1] - pytest.raises(Exception, Panel, data) + data['ItemB'] = self.panel['ItemB'].values[:, :-1] + pytest.raises(Exception, Panel, data) def test_ctor_orderedDict(self): - with catch_warnings(record=True): - 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) - assert list(p.items) == keys + 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) + assert list(p.items) == keys - p = Panel.from_dict(d) - assert list(p.items) == keys + p = Panel.from_dict(d) + assert list(p.items) == keys def test_constructor_resize(self): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - df = tm.makeDataFrame() - df['foo'] = 'bar' + df = tm.makeDataFrame() + df['foo'] = 'bar' - data = {'k1': df, 'k2': df} + data = {'k1': df, 'k2': df} - panel = Panel.from_dict(data, orient='minor') + panel = Panel.from_dict(data, orient='minor') - assert panel['foo'].values.dtype == np.object_ - assert panel['A'].values.dtype == np.float64 + assert panel['foo'].values.dtype == np.object_ + assert panel['A'].values.dtype == np.float64 def test_constructor_error_msgs(self): - with catch_warnings(record=True): - def testit(): - Panel(np.random.randn(3, 4, 5), - lrange(4), lrange(5), lrange(5)) - - tm.assert_raises_regex(ValueError, - r"Shape of passed values is " - r"\(3, 4, 5\), indices imply " - r"\(4, 5, 5\)", - testit) - - def testit(): - Panel(np.random.randn(3, 4, 5), - lrange(5), lrange(4), lrange(5)) - - tm.assert_raises_regex(ValueError, - r"Shape of passed values is " - r"\(3, 4, 5\), indices imply " - r"\(5, 4, 5\)", - testit) - - def testit(): - Panel(np.random.randn(3, 4, 5), - lrange(5), lrange(5), lrange(4)) - - tm.assert_raises_regex(ValueError, - r"Shape of passed values is " - r"\(3, 4, 5\), indices imply " - r"\(5, 5, 4\)", - testit) + def testit(): + Panel(np.random.randn(3, 4, 5), + lrange(4), lrange(5), lrange(5)) + + tm.assert_raises_regex(ValueError, + r"Shape of passed values is " + r"\(3, 4, 5\), indices imply " + r"\(4, 5, 5\)", + testit) + + def testit(): + Panel(np.random.randn(3, 4, 5), + lrange(5), lrange(4), lrange(5)) + + tm.assert_raises_regex(ValueError, + r"Shape of passed values is " + r"\(3, 4, 5\), indices imply " + r"\(5, 4, 5\)", + testit) + + def testit(): + Panel(np.random.randn(3, 4, 5), + lrange(5), lrange(5), lrange(4)) + + tm.assert_raises_regex(ValueError, + r"Shape of passed values is " + r"\(3, 4, 5\), indices imply " + r"\(5, 5, 4\)", + testit) def test_conform(self): - with catch_warnings(record=True): - df = self.panel['ItemA'][:-5].filter(items=['A', 'B']) - conformed = self.panel.conform(df) + 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) + 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): - with catch_warnings(record=True): - - # 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) + # 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): @@ -1222,964 +1195,933 @@ def test_dtypes(self): assert_series_equal(result, expected) def test_astype(self): - with catch_warnings(record=True): - # GH7271 - data = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) - panel = Panel(data, ['a', 'b'], ['c', 'd'], ['e', 'f']) + # 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) + 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) - pytest.raises(NotImplementedError, panel.astype, {0: str}) + pytest.raises(NotImplementedError, panel.astype, {0: str}) def test_apply(self): - with catch_warnings(record=True): - # 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) + # 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): - with catch_warnings(record=True): - - # 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) + + # 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(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 - result = self.panel.apply(f, axis=['items', 'major_axis']) - expected = Panel({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({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({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) + result = self.panel.apply(f, axis=['items', 'major_axis']) + expected = Panel({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({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({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): - with catch_warnings(record=True): - # GH10332 - self.panel = Panel(np.random.rand(5, 5, 5)) + # 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]) + 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) + 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) + 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): - with catch_warnings(record=True): - ref = self.panel['ItemB'] + ref = self.panel['ItemB'] - # items - result = self.panel.reindex(items=['ItemA', 'ItemB']) - assert_frame_equal(result['ItemB'], ref) + # 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)) + # 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 - pytest.raises(Exception, self.panel.reindex, - major_axis=new_major, - major=new_major) + # raise exception put both major and major_axis + pytest.raises(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)) + # 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)) - # raise exception put both major and major_axis - pytest.raises(Exception, self.panel.reindex, - minor_axis=new_minor, - minor=new_minor) + # raise exception put both major and major_axis + pytest.raises(Exception, self.panel.reindex, + minor_axis=new_minor, + minor=new_minor) - # this ok - result = self.panel.reindex() - assert_panel_equal(result, self.panel) - assert result is not self.panel + # this ok + result = self.panel.reindex() + assert_panel_equal(result, self.panel) + assert result is not self.panel - # with filling - smaller_major = self.panel.major_axis[::5] - smaller = self.panel.reindex(major=smaller_major) + # 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') + 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])) + 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) - assert result is self.panel + # don't necessarily copy + result = self.panel.reindex( + major=self.panel.major_axis, copy=False) + assert_panel_equal(result, self.panel) + assert result is self.panel def test_reindex_axis_style(self): - with catch_warnings(record=True): - panel = Panel(np.random.rand(5, 5, 5)) - expected0 = Panel(panel.values).iloc[[0, 1]] - expected1 = Panel(panel.values).iloc[:, [0, 1]] - expected2 = Panel(panel.values).iloc[:, :, [0, 1]] + panel = Panel(np.random.rand(5, 5, 5)) + expected0 = Panel(panel.values).iloc[[0, 1]] + expected1 = Panel(panel.values).iloc[:, [0, 1]] + expected2 = Panel(panel.values).iloc[:, :, [0, 1]] - result = panel.reindex([0, 1], axis=0) - assert_panel_equal(result, expected0) + result = panel.reindex([0, 1], axis=0) + assert_panel_equal(result, expected0) - result = panel.reindex([0, 1], axis=1) - assert_panel_equal(result, expected1) + result = panel.reindex([0, 1], axis=1) + assert_panel_equal(result, expected1) - result = panel.reindex([0, 1], axis=2) - assert_panel_equal(result, expected2) + result = panel.reindex([0, 1], axis=2) + assert_panel_equal(result, expected2) - result = panel.reindex([0, 1], axis=2) - assert_panel_equal(result, expected2) + result = panel.reindex([0, 1], axis=2) + assert_panel_equal(result, expected2) def test_reindex_multi(self): - with catch_warnings(record=True): - - # 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) - - assert result.items is self.panel.items - assert result.major_axis is self.panel.major_axis - assert result.minor_axis is 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) + + # 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) + + assert result.items is self.panel.items + assert result.major_axis is self.panel.major_axis + assert result.minor_axis is 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): - with catch_warnings(record=True): - # 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) + # 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): - with catch_warnings(record=True): - # 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 == 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) + # 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 indices 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) + # neg indices 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) - pytest.raises(Exception, self.panel.take, [4, 0, 1, 2], axis=2) + pytest.raises(Exception, self.panel.take, [4, 0, 1, 2], axis=2) def test_sort_index(self): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - filled = self.panel.fillna(0) - assert 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) - - pytest.raises(ValueError, self.panel.fillna) - pytest.raises(ValueError, self.panel.fillna, 5, method='ffill') - - pytest.raises(TypeError, self.panel.fillna, [1, 2]) - pytest.raises(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 - pytest.raises(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) + filled = self.panel.fillna(0) + assert 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) + + pytest.raises(ValueError, self.panel.fillna) + pytest.raises(ValueError, self.panel.fillna, 5, method='ffill') + + pytest.raises(TypeError, self.panel.fillna, [1, 2]) + pytest.raises(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 + pytest.raises(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): - with catch_warnings(record=True): - assert_panel_equal(self.panel.ffill(), - self.panel.fillna(method='ffill')) - assert_panel_equal(self.panel.bfill(), - self.panel.fillna(method='bfill')) + 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): - with catch_warnings(record=True): - # #1823 - result = self.panel.truncate(before=None, after=None, axis='items') + # #1823 + result = self.panel.truncate(before=None, after=None, axis='items') - # it works! - result.fillna(value=0.0) + # it works! + result.fillna(value=0.0) def test_swapaxes(self): - with catch_warnings(record=True): - result = self.panel.swapaxes('items', 'minor') - assert result.items is self.panel.minor_axis + result = self.panel.swapaxes('items', 'minor') + assert result.items is self.panel.minor_axis - result = self.panel.swapaxes('items', 'major') - assert result.items is self.panel.major_axis + result = self.panel.swapaxes('items', 'major') + assert result.items is self.panel.major_axis - result = self.panel.swapaxes('major', 'minor') - assert result.major_axis is self.panel.minor_axis + result = self.panel.swapaxes('major', 'minor') + assert result.major_axis is 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) + 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) - assert result.items is self.panel.major_axis + # this should also work + result = self.panel.swapaxes(0, 1) + assert result.items is self.panel.major_axis - # this works, but return a copy - result = self.panel.swapaxes('items', 'items') - assert_panel_equal(self.panel, result) - assert id(self.panel) != id(result) + # this works, but return a copy + result = self.panel.swapaxes('items', 'items') + assert_panel_equal(self.panel, result) + assert id(self.panel) != id(result) def test_transpose(self): - with catch_warnings(record=True): - 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.assert_raises_regex(TypeError, - 'not enough/duplicate arguments'): - self.panel.transpose('minor', maj='major', minor='items') - - with tm.assert_raises_regex(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) - - pytest.raises(ValueError, self.panel.transpose, 0, 0, 1) + 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.assert_raises_regex(TypeError, + 'not enough/duplicate arguments'): + self.panel.transpose('minor', maj='major', minor='items') + + with tm.assert_raises_regex(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) + + pytest.raises(ValueError, self.panel.transpose, 0, 0, 1) def test_transpose_copy(self): - with catch_warnings(record=True): - 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 = 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 - assert notna(result.values[1, 0, 1]) + panel.values[0, 1, 1] = np.nan + assert notna(result.values[1, 0, 1]) def test_to_frame(self): - with catch_warnings(record=True): - # 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 - assert 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() - assert rdf.index.names == df.index.names - assert rdf.columns.names == df.columns.names + # 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 + assert 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() + assert rdf.index.names == df.index.names + assert rdf.columns.names == df.columns.names def test_to_frame_mixed(self): - with catch_warnings(record=True): - panel = self.panel.fillna(0) - panel['str'] = 'foo' - panel['bool'] = panel['ItemA'] > 0 - - lp = panel.to_frame() - wp = lp.to_panel() - assert 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) + panel = self.panel.fillna(0) + panel['str'] = 'foo' + panel['bool'] = panel['ItemA'] > 0 + + lp = panel.to_frame() + wp = lp.to_panel() + assert 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): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - 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]]) + 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() - assert isna(panel[0].loc[1, [0, 1]]).all() + panel = df.to_panel() + assert isna(panel[0].loc[1, [0, 1]]).all() def test_to_panel_duplicates(self): # #2441 - with catch_warnings(record=True): - df = DataFrame({'a': [0, 0, 1], 'b': [1, 1, 1], 'c': [1, 2, 3]}) - idf = df.set_index(['a', 'b']) - tm.assert_raises_regex( - ValueError, 'non-uniquely indexed', idf.to_panel) + df = DataFrame({'a': [0, 0, 1], 'b': [1, 1, 1], 'c': [1, 2, 3]}) + idf = df.set_index(['a', 'b']) + tm.assert_raises_regex( + ValueError, 'non-uniquely indexed', idf.to_panel) def test_panel_dups(self): - with catch_warnings(record=True): - # GH 4960 - # duplicates in an index + # 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")) + # 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['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['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) + 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")) + # 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[:, '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[:, '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) + 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")) + # 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[:, :, '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[:, :, '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) + 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): - with catch_warnings(record=True): - compounded = self.panel.compound() + compounded = self.panel.compound() - assert_series_equal(compounded['ItemA'], - (1 + self.panel['ItemA']).product(0) - 1, - check_names=False) + assert_series_equal(compounded['ItemA'], + (1 + self.panel['ItemA']).product(0) - 1, + check_names=False) def test_shift(self): - with catch_warnings(record=True): - # 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({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) + # 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({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 - with catch_warnings(record=True): - ps = tm.makePeriodPanel() - shifted = ps.tshift(1) - unshifted = shifted.tshift(-1) + ps = tm.makePeriodPanel() + shifted = ps.tshift(1) + unshifted = shifted.tshift(-1) - assert_panel_equal(unshifted, ps) + assert_panel_equal(unshifted, ps) - shifted2 = ps.tshift(freq='B') - assert_panel_equal(shifted, shifted2) + shifted2 = ps.tshift(freq='B') + assert_panel_equal(shifted, shifted2) - shifted3 = ps.tshift(freq=BDay()) - assert_panel_equal(shifted, shifted3) + shifted3 = ps.tshift(freq=BDay()) + assert_panel_equal(shifted, shifted3) - tm.assert_raises_regex(ValueError, 'does not match', - ps.tshift, freq='M') + tm.assert_raises_regex(ValueError, 'does not match', + ps.tshift, freq='M') - # DatetimeIndex - panel = make_test_panel() - shifted = panel.tshift(1) - unshifted = shifted.tshift(-1) + # DatetimeIndex + panel = make_test_panel() + shifted = panel.tshift(1) + unshifted = shifted.tshift(-1) - assert_panel_equal(panel, unshifted) + assert_panel_equal(panel, unshifted) - shifted2 = panel.tshift(freq=panel.major_axis.freq) - assert_panel_equal(shifted, shifted2) + 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) + 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], :] - pytest.raises(ValueError, no_freq.tshift) + no_freq = panel.iloc[:, [0, 5, 7], :] + pytest.raises(ValueError, no_freq.tshift) def test_pct_change(self): - with catch_warnings(record=True): - 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) + 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): - with catch_warnings(record=True): - 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=date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - expected = Panel(evalues, items=['Item1', 'Item2'], - major_axis=date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - result = p.round() - assert_panel_equal(expected, result) + 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=date_range('1/1/2000', periods=5), + minor_axis=['A', 'B']) + expected = Panel(evalues, items=['Item1', 'Item2'], + major_axis=date_range('1/1/2000', periods=5), + minor_axis=['A', 'B']) + result = p.round() + assert_panel_equal(expected, result) def test_numpy_round(self): - with catch_warnings(record=True): - 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=date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - expected = Panel(evalues, items=['Item1', 'Item2'], - major_axis=date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - result = np.round(p) - assert_panel_equal(expected, result) - - msg = "the 'out' parameter is not supported" - tm.assert_raises_regex(ValueError, msg, np.round, p, out=p) - + 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=date_range('1/1/2000', periods=5), + minor_axis=['A', 'B']) + expected = Panel(evalues, items=['Item1', 'Item2'], + major_axis=date_range('1/1/2000', periods=5), + minor_axis=['A', 'B']) + result = np.round(p) + assert_panel_equal(expected, result) + + msg = "the 'out' parameter is not supported" + tm.assert_raises_regex(ValueError, msg, np.round, p, out=p) + + # removing Panel before NumPy enforces, so just ignore + @pytest.mark.filterwarnings("ignore:Using a non-tuple:FutureWarning") def test_multiindex_get(self): - with catch_warnings(record=True): - 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) - - assert (f1.items == [1, 2]).all() - assert (f2.items == [1, 2]).all() - - MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)], - names=['first', 'second']) - + 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) + + assert (f1.items == [1, 2]).all() + assert (f2.items == [1, 2]).all() + + MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)], + names=['first', 'second']) + + @pytest.mark.filterwarnings("ignore:Using a non-tuple:FutureWarning") def test_multiindex_blocks(self): - with catch_warnings(record=True): - ind = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)], - names=['first', 'second']) - wp = Panel(self.panel._data) - wp.items = ind - f1 = wp['a'] - assert (f1.items == [1, 2]).all() + ind = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)], + names=['first', 'second']) + wp = Panel(self.panel._data) + wp.items = ind + f1 = wp['a'] + assert (f1.items == [1, 2]).all() - f1 = wp[('b', 1)] - assert (f1.columns == ['A', 'B', 'C', 'D']).all() + f1 = wp[('b', 1)] + assert (f1.columns == ['A', 'B', 'C', 'D']).all() def test_repr_empty(self): - with catch_warnings(record=True): - empty = Panel() - repr(empty) + empty = Panel() + repr(empty) + # ignore warning from us, because removing panel + @pytest.mark.filterwarnings("ignore:Using:FutureWarning") def test_rename(self): - with catch_warnings(record=True): - mapper = {'ItemA': 'foo', 'ItemB': 'bar', 'ItemC': 'baz'} + mapper = {'ItemA': 'foo', 'ItemB': 'bar', 'ItemC': 'baz'} - renamed = self.panel.rename_axis(mapper, axis=0) - exp = Index(['foo', 'bar', 'baz']) - tm.assert_index_equal(renamed.items, exp) + renamed = self.panel.rename_axis(mapper, axis=0) + exp = Index(['foo', 'bar', 'baz']) + tm.assert_index_equal(renamed.items, exp) - renamed = self.panel.rename_axis(str.lower, axis=2) - exp = Index(['a', 'b', 'c', 'd']) - tm.assert_index_equal(renamed.minor_axis, exp) + renamed = self.panel.rename_axis(str.lower, axis=2) + exp = Index(['a', 'b', 'c', 'd']) + tm.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. - assert (self.panel['ItemA'].values == 3).all() + # don't copy + renamed_nocopy = self.panel.rename_axis(mapper, axis=0, copy=False) + renamed_nocopy['foo'] = 3. + assert (self.panel['ItemA'].values == 3).all() def test_get_attr(self): assert_frame_equal(self.panel['ItemA'], self.panel.ItemA) @@ -2191,13 +2133,12 @@ def test_get_attr(self): assert_frame_equal(self.panel['i'], self.panel.i) def test_from_frame_level1_unsorted(self): - with catch_warnings(record=True): - 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()) + 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: @@ -2239,194 +2180,188 @@ def test_to_excel_xlsxwriter(self): recdf = reader.parse(str(item), index_col=0) assert_frame_equal(df, recdf) + @pytest.mark.filterwarnings("ignore:'.reindex:FutureWarning") def test_dropna(self): - with catch_warnings(record=True): - p = Panel(np.random.randn(4, 5, 6), major_axis=list('abcde')) - p.loc[:, ['b', 'd'], 0] = np.nan + 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) + 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) + 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.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 + 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() + exp = p.loc[['a', 'c', 'd']] + assert_panel_equal(result, exp) - result = p.dropna(how='all') - assert_panel_equal(result, p) + 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) + 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): - with catch_warnings(record=True): - df = DataFrame({"A": [1, 2], "B": [3, 4]}) - panel = Panel({"One": df, "Two": df}) + 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) + 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) - 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) - - pytest.raises(KeyError, 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) + 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) + + pytest.raises(KeyError, 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): - with catch_warnings(record=True): - 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.]]]) + 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.]]]) - assert_panel_equal(pan, expected) + 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): - with catch_warnings(record=True): - 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( - {'one': DataFrame([[1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]), - 'two': DataFrame([[3.6, 2., 3], - [1.5, np.nan, 7], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]) - } - ) - - assert_panel_equal(pan, expected) + 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( + {'one': DataFrame([[1.5, np.nan, 3.], + [1.5, np.nan, 3.], + [1.5, np.nan, 3.], + [1.5, np.nan, 3.]]), + 'two': DataFrame([[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_nooverwrite(self): - with catch_warnings(record=True): - 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.]]]) + 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) - assert_panel_equal(pan, expected) + 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): - with catch_warnings(record=True): - 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.]]]) + 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]) + other = Panel( + [[[3.6, 2., np.nan], [np.nan, np.nan, 7]]], items=[1]) - pan.update(other, filter_func=lambda x: x > 2) + 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.]]]) + 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) + assert_panel_equal(pan, expected) def test_update_raise(self): - with catch_warnings(record=True): - 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.]]]) + 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.]]]) - pytest.raises(Exception, pan.update, *(pan, ), - **{'raise_conflict': True}) + pytest.raises(Exception, pan.update, *(pan, ), + **{'raise_conflict': True}) def test_all_any(self): assert (self.panel.all(axis=0).values == nanall( @@ -2452,6 +2387,7 @@ def test_sort_values(self): pytest.raises(NotImplementedError, self.panel.sort_values, 'ItemA') +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestPanelFrame(object): """ Check that conversions to and from Panel to DataFrame work. @@ -2463,90 +2399,82 @@ def setup_method(self, method): self.unfiltered_panel = panel.to_frame(filter_observations=False) def test_ops_differently_indexed(self): - with catch_warnings(record=True): - # trying to set non-identically indexed panel - wp = self.panel.to_panel() - wp2 = wp.reindex(major=wp.major_axis[:-1]) - lp2 = wp2.to_frame() + # 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) + 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) + # 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): - with catch_warnings(record=True): - result = self.panel.mul(2) - expected = DataFrame.__mul__(self.panel, 2) - assert_frame_equal(result, expected) + result = self.panel.mul(2) + expected = DataFrame.__mul__(self.panel, 2) + assert_frame_equal(result, expected) def test_combineFrame(self): - with catch_warnings(record=True): - wp = self.panel.to_panel() - result = self.panel.add(wp['ItemA'].stack(), axis=0) - assert_frame_equal(result.to_panel()['ItemA'], wp['ItemA'] * 2) + 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): - with catch_warnings(record=True): - 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']) + 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'])) + # one item + result = self.panel.add(self.panel.filter(['ItemA'])) def test_combine_scalar(self): - with catch_warnings(record=True): - result = self.panel.mul(2) - expected = DataFrame(self.panel._data) * 2 - assert_frame_equal(result, expected) + result = self.panel.mul(2) + expected = DataFrame(self.panel._data) * 2 + assert_frame_equal(result, expected) def test_combine_series(self): - with catch_warnings(record=True): - 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['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) + 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): - with catch_warnings(record=True): - wp = self.panel.to_panel() - result = (self.panel + 1).to_panel() - assert_frame_equal(wp['ItemA'] + 1, result['ItemA']) + 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): - with catch_warnings(record=True): - 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) + 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): @@ -2569,44 +2497,43 @@ def test_to_sparse(self): self.panel.to_sparse) def test_truncate(self): - with catch_warnings(record=True): - dates = self.panel.index.levels[0] - start, end = dates[1], dates[5] + 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) + trunced = self.panel.truncate(start, end).to_panel() + expected = self.panel.to_panel()['ItemA'].truncate(start, end) - # TODO truncate drops index.names - assert_frame_equal(trunced['ItemA'], expected, check_names=False) + # TODO truncate 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) + trunced = self.panel.truncate(before=start).to_panel() + expected = self.panel.to_panel()['ItemA'].truncate(before=start) - # TODO truncate drops index.names - assert_frame_equal(trunced['ItemA'], expected, check_names=False) + # TODO truncate 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) + trunced = self.panel.truncate(after=end).to_panel() + expected = self.panel.to_panel()['ItemA'].truncate(after=end) - # TODO truncate drops index.names - assert_frame_equal(trunced['ItemA'], expected, check_names=False) + # TODO truncate 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] + # 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) + wp2 = wp.reindex(major=new_index) - lp2 = wp2.to_frame() - lp_trunc = lp2.truncate(wp.major_axis[2], wp.major_axis[-2]) + 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]) + wp_trunc = wp2.truncate(wp.major_axis[2], wp.major_axis[-2]) - assert_panel_equal(wp_trunc, lp_trunc.to_panel()) + assert_panel_equal(wp_trunc, lp_trunc.to_panel()) - # throw proper exception - pytest.raises(Exception, lp2.truncate, wp.major_axis[-2], - wp.major_axis[2]) + # throw proper exception + pytest.raises(Exception, lp2.truncate, wp.major_axis[-2], + wp.major_axis[2]) def test_axis_dummies(self): from pandas.core.reshape.reshape import make_axis_dummies @@ -2635,46 +2562,42 @@ def test_get_dummies(self): tm.assert_numpy_array_equal(dummies.values, minor_dummies.values) def test_mean(self): - with catch_warnings(record=True): - means = self.panel.mean(level='minor') + means = self.panel.mean(level='minor') - # test versus Panel version - wide_means = self.panel.to_panel().mean('major') - assert_frame_equal(means, wide_means) + # test versus Panel version + wide_means = self.panel.to_panel().mean('major') + assert_frame_equal(means, wide_means) def test_sum(self): - with catch_warnings(record=True): - sums = self.panel.sum(level='minor') + sums = self.panel.sum(level='minor') - # test versus Panel version - wide_sums = self.panel.to_panel().sum('major') - assert_frame_equal(sums, wide_sums) + # test versus Panel version + wide_sums = self.panel.to_panel().sum('major') + assert_frame_equal(sums, wide_sums) def test_count(self): - with catch_warnings(record=True): - index = self.panel.index + index = self.panel.index - major_count = self.panel.count(level=0)['ItemA'] - labels = index.labels[0] - for i, idx in enumerate(index.levels[0]): - assert major_count[i] == (labels == i).sum() + major_count = self.panel.count(level=0)['ItemA'] + labels = index.labels[0] + for i, idx in enumerate(index.levels[0]): + assert 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]): - assert minor_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]): + assert minor_count[i] == (labels == i).sum() def test_join(self): - with catch_warnings(record=True): - lp1 = self.panel.filter(['ItemA', 'ItemB']) - lp2 = self.panel.filter(['ItemC']) + lp1 = self.panel.filter(['ItemA', 'ItemB']) + lp2 = self.panel.filter(['ItemC']) - joined = lp1.join(lp2) + joined = lp1.join(lp2) - assert len(joined.columns) == 3 + assert len(joined.columns) == 3 - pytest.raises(Exception, lp1.join, - self.panel.filter(['ItemB', 'ItemC'])) + pytest.raises(Exception, lp1.join, + self.panel.filter(['ItemB', 'ItemC'])) def test_panel_index(): @@ -2685,8 +2608,8 @@ def test_panel_index(): tm.assert_index_equal(index, expected) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_panel_np_all(): - with catch_warnings(record=True): - wp = Panel({"A": DataFrame({'b': [1, 2]})}) + wp = Panel({"A": DataFrame({'b': [1, 2]})}) result = np.all(wp) assert result == np.bool_(True) diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index 669fa9742a705..377253574d2c1 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -1,6 +1,6 @@ # pylint: disable=E1101 -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter from datetime import datetime, timedelta from functools import partial from textwrap import dedent @@ -1463,6 +1463,7 @@ def test_resample_panel(self): n = len(rng) with catch_warnings(record=True): + simplefilter("ignore", FutureWarning) panel = Panel(np.random.randn(3, n, 5), items=['one', 'two', 'three'], major_axis=rng, @@ -1485,6 +1486,7 @@ def p_apply(panel, f): lambda x: x.resample('M', axis=1).mean()) tm.assert_panel_equal(result, expected) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_resample_panel_numpy(self): rng = date_range('1/1/2000', '6/30/2000') n = len(rng) @@ -3237,25 +3239,25 @@ def test_apply_iteration(self): result = grouped.apply(f) tm.assert_index_equal(result.index, df.index) + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_panel_aggregation(self): ind = pd.date_range('1/1/2000', periods=100) data = np.random.randn(2, len(ind), 4) - with catch_warnings(record=True): - wp = Panel(data, items=['Item1', 'Item2'], major_axis=ind, - minor_axis=['A', 'B', 'C', 'D']) + wp = 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() + 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) + def f(x): + assert (isinstance(x, Panel)) + return x.mean(1) - result = bingrouped.agg(f) - tm.assert_panel_equal(result, binagg) + result = bingrouped.agg(f) + tm.assert_panel_equal(result, binagg) def test_fails_on_no_datetime_index(self): index_names = ('Int64Index', 'Index', 'Float64Index', 'MultiIndex') diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index ec6d83062c8b0..052bfd2b858fb 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -153,6 +153,8 @@ def test_agg(self): tm.assert_frame_equal(result, expected) 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'), @@ -160,6 +162,7 @@ def test_agg(self): tm.assert_frame_equal(result, expected, check_like=True) with catch_warnings(record=True): + warnings.simplefilter("ignore", FutureWarning) result = r.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}, 'B': {'mean2': 'mean', @@ -223,11 +226,13 @@ def f(): expected.columns = pd.MultiIndex.from_tuples([('ra', 'mean'), ( 'ra', 'std'), ('rb', 'mean'), ('rb', '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) 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'), ( @@ -278,6 +283,7 @@ def test_count_nonnumeric_types(self): 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): # make sure that we are aggregating window functions correctly with arg r = Series(np.random.randn(100)).rolling(window=10, min_periods=1, @@ -309,6 +315,7 @@ def test_preserve_metadata(self): assert s3.name == 'foo' +@pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") class TestWindow(Base): def setup_method(self, method): @@ -940,6 +947,7 @@ 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_method(self, method): @@ -1901,6 +1909,7 @@ def test_no_pairwise_with_other(self, f): 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) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index f9f5fc2484bda..b8fabbf52159d 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1825,6 +1825,7 @@ def test_weekmask_and_holidays(self): 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) @@ -1987,6 +1988,7 @@ def test_holidays(self): 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() @@ -2105,6 +2107,7 @@ def test_holidays(self): 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) diff --git a/pandas/tests/tseries/offsets/test_offsets_properties.py b/pandas/tests/tseries/offsets/test_offsets_properties.py index f19066ba76b20..07a6895d1e231 100644 --- a/pandas/tests/tseries/offsets/test_offsets_properties.py +++ b/pandas/tests/tseries/offsets/test_offsets_properties.py @@ -8,6 +8,7 @@ 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 import pytest from hypothesis import given, assume, strategies as st @@ -25,6 +26,11 @@ # ---------------------------------------------------------------- # 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( @@ -38,8 +44,8 @@ ) gen_random_datetime = st.datetimes( - min_value=pd.Timestamp.min.to_pydatetime(), - max_value=pd.Timestamp.max.to_pydatetime(), + min_value=min_dt, + max_value=max_dt, timezones=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()) ) diff --git a/pandas/tests/tslibs/test_parsing.py b/pandas/tests/tslibs/test_parsing.py index 14c9ca1f6cc54..466a22e5916e9 100644 --- a/pandas/tests/tslibs/test_parsing.py +++ b/pandas/tests/tslibs/test_parsing.py @@ -92,6 +92,7 @@ def test_parsers_monthfreq(self): assert result1 == expected +@pytest.mark.filterwarnings("ignore:_timelex:DeprecationWarning") class TestGuessDatetimeFormat(object): @td.skip_if_not_us_locale @@ -160,6 +161,8 @@ def test_guess_datetime_format_invalid_inputs(self): ('2011-1-1 00:00:00', '%Y-%m-%d %H:%M:%S'), ('2011-1-1 0:0:0', '%Y-%m-%d %H:%M:%S'), ('2011-1-3T00:00:0', '%Y-%m-%dT%H:%M:%S')]) + # https://github.com/pandas-dev/pandas/issues/21322 for _timelex + @pytest.mark.filterwarnings("ignore:_timelex:DeprecationWarning") def test_guess_datetime_format_nopadding(self, string, format): # GH 11142 result = parsing._guess_datetime_format(string) diff --git a/pandas/tests/util/test_hashing.py b/pandas/tests/util/test_hashing.py index 0c14dcb49c56f..b62260071d996 100644 --- a/pandas/tests/util/test_hashing.py +++ b/pandas/tests/util/test_hashing.py @@ -1,7 +1,6 @@ import pytest import datetime -from warnings import catch_warnings import numpy as np import pandas as pd @@ -216,12 +215,12 @@ def test_categorical_with_nan_consistency(self): assert result[0] in expected assert result[1] in expected + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_pandas_errors(self): with pytest.raises(TypeError): hash_pandas_object(pd.Timestamp('20130101')) - with catch_warnings(record=True): - obj = tm.makePanel() + obj = tm.makePanel() with pytest.raises(TypeError): hash_pandas_object(obj) diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py index 33dcf6d64b302..b9c89c4e314f9 100644 --- a/pandas/tseries/holiday.py +++ b/pandas/tseries/holiday.py @@ -1,6 +1,7 @@ import warnings from pandas import DateOffset, DatetimeIndex, Series, Timestamp +from pandas.errors import PerformanceWarning from pandas.compat import add_metaclass from datetime import datetime, timedelta from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU # noqa @@ -281,7 +282,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 diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 1e8c123fa6f13..edd0b0aa82d23 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -205,8 +205,12 @@ def decompress_file(path, compression): msg = 'Unrecognized compression type: {}'.format(compression) raise ValueError(msg) - yield f - f.close() + try: + yield f + finally: + f.close() + if compression == "zip": + zip_file.close() def assert_almost_equal(left, right, check_dtype="equiv", @@ -1897,6 +1901,7 @@ def makePeriodFrame(nper=None): def makePanel(nper=None): with warnings.catch_warnings(record=True): + warnings.filterwarnings("ignore", "\\nPanel", FutureWarning) cols = ['Item' + c for c in string.ascii_uppercase[:K - 1]] data = {c: makeTimeDataFrame(nper) for c in cols} return Panel.fromDict(data) @@ -1904,6 +1909,7 @@ def makePanel(nper=None): def makePeriodPanel(nper=None): with warnings.catch_warnings(record=True): + warnings.filterwarnings("ignore", "\\nPanel", FutureWarning) cols = ['Item' + c for c in string.ascii_uppercase[:K - 1]] data = {c: makePeriodFrame(nper) for c in cols} return Panel.fromDict(data) diff --git a/setup.cfg b/setup.cfg index 021159bad99de..fb42dfd3b6d15 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,8 +40,7 @@ markers = 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 - +addopts = --strict-data-files --durations=10 [coverage:run] branch = False From bf29988592ef487c7149d393d2333242c9f78868 Mon Sep 17 00:00:00 2001 From: Jay Offerdahl Date: Wed, 19 Sep 2018 09:16:10 -0500 Subject: [PATCH 24/87] BUG: Allow IOErrors when attempting to retrieve default client encoding. (#21531) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/io/formats/console.py | 2 +- pandas/tests/io/formats/test_console.py | 74 +++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 pandas/tests/io/formats/test_console.py diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3a44b0260153c..d1ede31fd5d1d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -749,6 +749,7 @@ I/O - :func:`read_sas()` will parse numbers in sas7bdat-files that have width less than 8 bytes correctly. (:issue:`21616`) - :func:`read_sas()` will correctly parse sas7bdat files with many columns (:issue:`22628`) - :func:`read_sas()` will correctly parse sas7bdat files with data page types having also bit 7 set (so page type is 128 + 256 = 384) (:issue:`16615`) +- Bug in :meth:`detect_client_encoding` where potential ``IOError`` goes unhandled when importing in a mod_wsgi process due to restricted access to stdout. (:issue:`21552`) Plotting ^^^^^^^^ diff --git a/pandas/io/formats/console.py b/pandas/io/formats/console.py index 45d50ea3fa073..b8b28a0b0c98c 100644 --- a/pandas/io/formats/console.py +++ b/pandas/io/formats/console.py @@ -21,7 +21,7 @@ def detect_console_encoding(): encoding = None try: encoding = sys.stdout.encoding or sys.stdin.encoding - except AttributeError: + except (AttributeError, IOError): pass # try again for something better diff --git a/pandas/tests/io/formats/test_console.py b/pandas/tests/io/formats/test_console.py new file mode 100644 index 0000000000000..055763bf62d6e --- /dev/null +++ b/pandas/tests/io/formats/test_console.py @@ -0,0 +1,74 @@ +import pytest + +from pandas.io.formats.console import detect_console_encoding + + +class MockEncoding(object): # TODO(py27): replace with mock + """ + Used to add a side effect when accessing the 'encoding' property. If the + side effect is a str in nature, the value will be returned. Otherwise, the + side effect should be an exception that will be raised. + """ + def __init__(self, encoding): + super(MockEncoding, self).__init__() + self.val = encoding + + @property + def encoding(self): + return self.raise_or_return(self.val) + + @staticmethod + def raise_or_return(val): + if isinstance(val, str): + return val + else: + raise val + + +@pytest.mark.parametrize('empty,filled', [ + ['stdin', 'stdout'], + ['stdout', 'stdin'] +]) +def test_detect_console_encoding_from_stdout_stdin(monkeypatch, empty, filled): + # Ensures that when sys.stdout.encoding or sys.stdin.encoding is used when + # they have values filled. + # GH 21552 + with monkeypatch.context() as context: + context.setattr('sys.{}'.format(empty), MockEncoding('')) + context.setattr('sys.{}'.format(filled), MockEncoding(filled)) + assert detect_console_encoding() == filled + + +@pytest.mark.parametrize('encoding', [ + AttributeError, + IOError, + 'ascii' +]) +def test_detect_console_encoding_fallback_to_locale(monkeypatch, encoding): + # GH 21552 + with monkeypatch.context() as context: + context.setattr('locale.getpreferredencoding', lambda: 'foo') + context.setattr('sys.stdout', MockEncoding(encoding)) + assert detect_console_encoding() == 'foo' + + +@pytest.mark.parametrize('std,locale', [ + ['ascii', 'ascii'], + ['ascii', Exception], + [AttributeError, 'ascii'], + [AttributeError, Exception], + [IOError, 'ascii'], + [IOError, Exception] +]) +def test_detect_console_encoding_fallback_to_default(monkeypatch, std, locale): + # When both the stdout/stdin encoding and locale preferred encoding checks + # fail (or return 'ascii', we should default to the sys default encoding. + # GH 21552 + with monkeypatch.context() as context: + context.setattr( + 'locale.getpreferredencoding', + lambda: MockEncoding.raise_or_return(locale) + ) + context.setattr('sys.stdout', MockEncoding(std)) + context.setattr('sys.getdefaultencoding', lambda: 'sysDefaultEncoding') + assert detect_console_encoding() == 'sysDefaultEncoding' From 9f4ccb59ad7e26ecf17847c8e17474bd26474f69 Mon Sep 17 00:00:00 2001 From: alimcmaster1 Date: Wed, 19 Sep 2018 15:23:26 +0100 Subject: [PATCH 25/87] API: Git version (#22745) --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/__init__.py | 1 + pandas/tests/test_common.py | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index d1ede31fd5d1d..487d5d0d2accd 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -185,7 +185,7 @@ Other Enhancements - :class:`Resampler` now is iterable like :class:`GroupBy` (:issue:`15314`). - :meth:`Series.resample` and :meth:`DataFrame.resample` have gained the :meth:`Resampler.quantile` (:issue:`15023`). - :meth:`Index.to_frame` now supports overriding column name(s) (:issue:`22580`). - +- New attribute :attr:`__git_version__` will return git commit sha of current build (:issue:`21295`). .. _whatsnew_0240.api_breaking: Backwards incompatible API changes diff --git a/pandas/__init__.py b/pandas/__init__.py index f91d0aa84e0ff..e446782d9665e 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -80,6 +80,7 @@ from ._version import get_versions v = get_versions() __version__ = v.get('closest-tag', v['version']) +__git_version__ = v.get('full-revisionid') del get_versions, v # module level doc-string diff --git a/pandas/tests/test_common.py b/pandas/tests/test_common.py index 868525e818b62..ae46bee901ff2 100644 --- a/pandas/tests/test_common.py +++ b/pandas/tests/test_common.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- import collections +import string from functools import partial import numpy as np import pytest +import pandas as pd from pandas import Series, Timestamp from pandas.core import ( common as com, @@ -110,3 +112,10 @@ def test_standardize_mapping(): 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) From 51aeba4b9b7cf1fdfe0a3b8b922e0ad39ff403fe Mon Sep 17 00:00:00 2001 From: topper-123 Date: Wed, 19 Sep 2018 15:39:46 +0100 Subject: [PATCH 26/87] DOC: add more links to the API in advanced.rst (#22746) --- doc/source/advanced.rst | 63 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/doc/source/advanced.rst b/doc/source/advanced.rst index 611afb3670ebc..835c4cc9d4ab3 100644 --- a/doc/source/advanced.rst +++ b/doc/source/advanced.rst @@ -15,7 +15,8 @@ MultiIndex / Advanced Indexing ****************************** -This section covers indexing with a ``MultiIndex`` and :ref:`more advanced indexing features `. +This section covers :ref:`indexing with a MultiIndex ` +and :ref:`other advanced indexing features `. See the :ref:`Indexing and Selecting Data ` for general indexing documentation. @@ -37,7 +38,7 @@ Hierarchical / Multi-level indexing is very exciting as it opens the door to som quite sophisticated data analysis and manipulation, especially for working with higher dimensional data. In essence, it enables you to store and manipulate data with an arbitrary number of dimensions in lower dimensional data -structures like Series (1d) and DataFrame (2d). +structures like ``Series`` (1d) and ``DataFrame`` (2d). In this section, we will show what exactly we mean by "hierarchical" indexing and how it integrates with all of the pandas indexing functionality @@ -83,8 +84,8 @@ to use the :meth:`MultiIndex.from_product` method: iterables = [['bar', 'baz', 'foo', 'qux'], ['one', 'two']] pd.MultiIndex.from_product(iterables, names=['first', 'second']) -As a convenience, you can pass a list of arrays directly into Series or -DataFrame to construct a ``MultiIndex`` automatically: +As a convenience, you can pass a list of arrays directly into ``Series`` or +``DataFrame`` to construct a ``MultiIndex`` automatically: .. ipython:: python @@ -213,8 +214,8 @@ tuples: s + s[:-2] s + s[::2] -``reindex`` can be called with another ``MultiIndex``, or even a list or array -of tuples: +The :meth:`~DataFrame.reindex` method of ``Series``/``DataFrames`` can be +called with another ``MultiIndex``, or even a list or array of tuples: .. ipython:: python @@ -413,7 +414,7 @@ selecting data at a particular level of a ``MultiIndex`` easier. # using the slicers df.loc[(slice(None),'one'),:] -You can also select on the columns with :meth:`~pandas.MultiIndex.xs`, by +You can also select on the columns with ``xs``, by providing the axis argument. .. ipython:: python @@ -426,7 +427,7 @@ providing the axis argument. # using the slicers df.loc[:,(slice(None),'one')] -:meth:`~pandas.MultiIndex.xs` also allows selection with multiple keys. +``xs`` also allows selection with multiple keys. .. ipython:: python @@ -437,7 +438,7 @@ providing the axis argument. # using the slicers df.loc[:,('bar','one')] -You can pass ``drop_level=False`` to :meth:`~pandas.MultiIndex.xs` to retain +You can pass ``drop_level=False`` to ``xs`` to retain the level that was selected. .. ipython:: python @@ -460,9 +461,9 @@ Compare the above with the result using ``drop_level=True`` (the default value). Advanced reindexing and alignment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The parameter ``level`` has been added to the ``reindex`` and ``align`` methods -of pandas objects. This is useful to broadcast values across a level. For -instance: +Using the parameter ``level`` in the :meth:`~DataFrame.reindex` and +:meth:`~DataFrame.align` methods of pandas objects is useful to broadcast +values across a level. For instance: .. ipython:: python @@ -480,10 +481,10 @@ instance: df2_aligned -Swapping levels with :meth:`~pandas.MultiIndex.swaplevel` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Swapping levels with ``swaplevel`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``swaplevel`` function can switch the order of two levels: +The :meth:`~MultiIndex.swaplevel` method can switch the order of two levels: .. ipython:: python @@ -492,21 +493,21 @@ The ``swaplevel`` function can switch the order of two levels: .. _advanced.reorderlevels: -Reordering levels with :meth:`~pandas.MultiIndex.reorder_levels` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reordering levels with ``reorder_levels`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``reorder_levels`` function generalizes the ``swaplevel`` function, -allowing you to permute the hierarchical index levels in one step: +The :meth:`~MultiIndex.reorder_levels` method generalizes the ``swaplevel`` +method, allowing you to permute the hierarchical index levels in one step: .. ipython:: python df[:5].reorder_levels([1,0], axis=0) -Sorting a :class:`~pandas.MultiIndex` -------------------------------------- +Sorting a ``MultiIndex`` +------------------------ -For MultiIndex-ed objects to be indexed and sliced effectively, they need -to be sorted. As with any index, you can use ``sort_index``. +For :class:`MultiIndex`-ed objects to be indexed and sliced effectively, +they need to be sorted. As with any index, you can use :meth:`~DataFrame.sort_index`. .. ipython:: python @@ -658,9 +659,9 @@ faster than fancy indexing. Index Types ----------- -We have discussed ``MultiIndex`` in the previous sections pretty extensively. ``DatetimeIndex`` and ``PeriodIndex`` -are shown :ref:`here `, and information about -``TimedeltaIndex`` is found :ref:`here `. +We have discussed ``MultiIndex`` in the previous sections pretty extensively. +Documentation about ``DatetimeIndex`` and ``PeriodIndex`` are shown :ref:`here `, +and documentation about ``TimedeltaIndex`` is found :ref:`here `. In the following sub-sections we will highlight some other index types. @@ -1004,8 +1005,8 @@ Non-monotonic indexes require exact matches If the index of a ``Series`` or ``DataFrame`` is monotonically increasing or decreasing, then the bounds of a label-based slice can be outside the range of the index, much like slice indexing a -normal Python ``list``. Monotonicity of an index can be tested with the ``is_monotonic_increasing`` and -``is_monotonic_decreasing`` attributes. +normal Python ``list``. Monotonicity of an index can be tested with the :meth:`~Index.is_monotonic_increasing` and +:meth:`~Index.is_monotonic_decreasing` attributes. .. ipython:: python @@ -1039,9 +1040,9 @@ On the other hand, if the index is not monotonic, then both slice bounds must be In [11]: df.loc[2:3, :] KeyError: 'Cannot get right slice bound for non-unique label: 3' -:meth:`Index.is_monotonic_increasing` and :meth:`Index.is_monotonic_decreasing` only check that +``Index.is_monotonic_increasing`` and ``Index.is_monotonic_decreasing`` only check that an index is weakly monotonic. To check for strict monotonicity, you can combine one of those with -:meth:`Index.is_unique` +the :meth:`~Index.is_unique` attribute. .. ipython:: python @@ -1057,7 +1058,7 @@ Compared with standard Python sequence slicing in which the slice endpoint is not inclusive, label-based slicing in pandas **is inclusive**. The primary reason for this is that it is often not possible to easily determine the "successor" or next element after a particular label in an index. For example, -consider the following Series: +consider the following ``Series``: .. ipython:: python From d923385c77d85e85910ac8d7835c29b85b5230b6 Mon Sep 17 00:00:00 2001 From: Thierry Moisan Date: Wed, 19 Sep 2018 11:37:36 -0400 Subject: [PATCH 27/87] DOC: Fix DataFrame.to_xarray doctests and allow the CI to run it. (#22673) --- ci/doctests.sh | 2 +- pandas/core/generic.py | 114 +++++++++++++++++++---------------------- 2 files changed, 53 insertions(+), 63 deletions(-) diff --git a/ci/doctests.sh b/ci/doctests.sh index a941515fde4ae..e7fe80e60eb6d 100755 --- a/ci/doctests.sh +++ b/ci/doctests.sh @@ -35,7 +35,7 @@ if [ "$DOCTEST" ]; then fi pytest --doctest-modules -v pandas/core/generic.py \ - -k"-_set_axis_name -_xs -describe -droplevel -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -resample -sample -to_json -to_xarray -transpose -values -xs" + -k"-_set_axis_name -_xs -describe -droplevel -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -resample -sample -to_json -transpose -values -xs" if [ $? -ne "0" ]; then RET=1 diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 373830ec7892e..3f7334131e146 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -2500,80 +2500,70 @@ def to_xarray(self): Returns ------- - a DataArray for a Series - a Dataset for a DataFrame - a DataArray for higher dims + xarray.DataArray or xarray.Dataset + Data in the pandas structure converted to Dataset if the object is + a DataFrame, or a DataArray if the object is a Series. + + See Also + -------- + DataFrame.to_hdf : Write DataFrame to an HDF5 file. + DataFrame.to_parquet : Write a DataFrame to the binary parquet format. Examples -------- - >>> df = pd.DataFrame({'A' : [1, 1, 2], - 'B' : ['foo', 'bar', 'foo'], - 'C' : np.arange(4.,7)}) + >>> df = pd.DataFrame([('falcon', 'bird', 389.0, 2), + ... ('parrot', 'bird', 24.0, 2), + ... ('lion', 'mammal', 80.5, 4), + ... ('monkey', 'mammal', np.nan, 4)], + ... columns=['name', 'class', 'max_speed', + ... 'num_legs']) >>> df - A B C - 0 1 foo 4.0 - 1 1 bar 5.0 - 2 2 foo 6.0 + name class max_speed num_legs + 0 falcon bird 389.0 2 + 1 parrot bird 24.0 2 + 2 lion mammal 80.5 4 + 3 monkey mammal NaN 4 >>> df.to_xarray() - Dimensions: (index: 3) + Dimensions: (index: 4) Coordinates: - * index (index) int64 0 1 2 + * index (index) int64 0 1 2 3 Data variables: - A (index) int64 1 1 2 - B (index) object 'foo' 'bar' 'foo' - C (index) float64 4.0 5.0 6.0 - - >>> df = pd.DataFrame({'A' : [1, 1, 2], - 'B' : ['foo', 'bar', 'foo'], - 'C' : np.arange(4.,7)} - ).set_index(['B','A']) - >>> df - C - B A - foo 1 4.0 - bar 1 5.0 - foo 2 6.0 - - >>> df.to_xarray() + name (index) object 'falcon' 'parrot' 'lion' 'monkey' + class (index) object 'bird' 'bird' 'mammal' 'mammal' + max_speed (index) float64 389.0 24.0 80.5 nan + num_legs (index) int64 2 2 4 4 + + >>> df['max_speed'].to_xarray() + + array([389. , 24. , 80.5, nan]) + Coordinates: + * index (index) int64 0 1 2 3 + + >>> dates = pd.to_datetime(['2018-01-01', '2018-01-01', + ... '2018-01-02', '2018-01-02']) + >>> df_multiindex = pd.DataFrame({'date': dates, + ... 'animal': ['falcon', 'parrot', 'falcon', + ... 'parrot'], + ... 'speed': [350, 18, 361, 15]}).set_index(['date', + ... 'animal']) + >>> df_multiindex + speed + date animal + 2018-01-01 falcon 350 + parrot 18 + 2018-01-02 falcon 361 + parrot 15 + + >>> df_multiindex.to_xarray() - Dimensions: (A: 2, B: 2) + Dimensions: (animal: 2, date: 2) Coordinates: - * B (B) object 'bar' 'foo' - * A (A) int64 1 2 + * date (date) datetime64[ns] 2018-01-01 2018-01-02 + * animal (animal) object 'falcon' 'parrot' Data variables: - C (B, A) float64 5.0 nan 4.0 6.0 - - >>> p = pd.Panel(np.arange(24).reshape(4,3,2), - items=list('ABCD'), - major_axis=pd.date_range('20130101', periods=3), - minor_axis=['first', 'second']) - >>> p - - Dimensions: 4 (items) x 3 (major_axis) x 2 (minor_axis) - Items axis: A to D - Major_axis axis: 2013-01-01 00:00:00 to 2013-01-03 00:00:00 - Minor_axis axis: first to second - - >>> p.to_xarray() - - array([[[ 0, 1], - [ 2, 3], - [ 4, 5]], - [[ 6, 7], - [ 8, 9], - [10, 11]], - [[12, 13], - [14, 15], - [16, 17]], - [[18, 19], - [20, 21], - [22, 23]]]) - Coordinates: - * items (items) object 'A' 'B' 'C' 'D' - * major_axis (major_axis) datetime64[ns] 2013-01-01 2013-01-02 2013-01-03 # noqa - * minor_axis (minor_axis) object 'first' 'second' + speed (date, animal) int64 350 18 361 15 Notes ----- From f6ce3e727a55af80683c8c91c1f42527156b07b1 Mon Sep 17 00:00:00 2001 From: "azure-pipelines[bot]" Date: Wed, 19 Sep 2018 10:42:24 -0500 Subject: [PATCH 28/87] Set up CI with Azure Pipelines (#22760) --- .travis.yml | 5 - appveyor.yml | 91 ------------------ azure-pipelines.yml | 25 +++++ ci/{travis-35-osx.yaml => azure-macos-35.yml} | 0 ...appveyor-27.yaml => azure-windows-27.yaml} | 0 ...appveyor-36.yaml => azure-windows-36.yaml} | 0 ci/azure/macos.yml | 39 ++++++++ ci/azure/windows-py27.yml | 41 +++++++++ ci/azure/windows.yml | 32 +++++++ ci/incremental/build.cmd | 10 ++ ci/incremental/build.sh | 18 ++++ ci/incremental/install_miniconda.sh | 19 ++++ ci/incremental/setup_conda_environment.cmd | 21 +++++ ci/incremental/setup_conda_environment.sh | 48 ++++++++++ ci/install.ps1 | 92 ------------------- 15 files changed, 253 insertions(+), 188 deletions(-) delete mode 100644 appveyor.yml create mode 100644 azure-pipelines.yml rename ci/{travis-35-osx.yaml => azure-macos-35.yml} (100%) rename ci/{appveyor-27.yaml => azure-windows-27.yaml} (100%) rename ci/{appveyor-36.yaml => azure-windows-36.yaml} (100%) create mode 100644 ci/azure/macos.yml create mode 100644 ci/azure/windows-py27.yml create mode 100644 ci/azure/windows.yml 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 diff --git a/.travis.yml b/.travis.yml index 76f4715a4abb2..a180e83eeec21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,11 +30,6 @@ matrix: exclude: # Exclude the default Python 3.5 build - python: 3.5 - include: - - os: osx - language: generic - env: - - JOB="3.5, OSX" ENV_FILE="ci/travis-35-osx.yaml" TEST_ARGS="--skip-slow --skip-network" - dist: trusty env: diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index c6199c1493f22..0000000000000 --- a/appveyor.yml +++ /dev/null @@ -1,91 +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 interpreter - # 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 - PANDAS_TESTING_MODE: "deprecate" - - matrix: - - - CONDA_ROOT: "C:\\Miniconda3_64" - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - PYTHON_VERSION: "3.6" - PYTHON_ARCH: "64" - CONDA_PY: "36" - CONDA_NPY: "113" - - - CONDA_ROOT: "C:\\Miniconda3_64" - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - 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 env create -q -n pandas --file=ci\appveyor-%CONDA_PY%.yaml - - cmd: activate pandas - - cmd: conda list -n pandas - # uninstall pandas if it's present - - cmd: conda remove pandas -y --force & exit 0 - - cmd: pip uninstall -y pandas & exit 0 - - # 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/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000000000..c82dafa224961 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,25 @@ +# Adapted from https://github.com/numba/numba/blob/master/azure-pipelines.yml +jobs: +# Mac and Linux could potentially use the same template +# except it isn't clear how to use a different build matrix +# for each, so for now they are separate +- template: ci/azure/macos.yml + parameters: + name: macOS + vmImage: xcode9-macos10.13 +# - template: ci/azure/linux.yml +# parameters: +# name: Linux +# vmImage: ubuntu-16.04 + +# Windows Python 2.7 needs VC 9.0 installed, and not sure +# how to make that a conditional task, so for now these are +# separate templates as well +- template: ci/azure/windows.yml + parameters: + name: Windows + vmImage: vs2017-win2017 +- template: ci/azure/windows-py27.yml + parameters: + name: WindowsPy27 + vmImage: vs2017-win2017 diff --git a/ci/travis-35-osx.yaml b/ci/azure-macos-35.yml similarity index 100% rename from ci/travis-35-osx.yaml rename to ci/azure-macos-35.yml diff --git a/ci/appveyor-27.yaml b/ci/azure-windows-27.yaml similarity index 100% rename from ci/appveyor-27.yaml rename to ci/azure-windows-27.yaml diff --git a/ci/appveyor-36.yaml b/ci/azure-windows-36.yaml similarity index 100% rename from ci/appveyor-36.yaml rename to ci/azure-windows-36.yaml diff --git a/ci/azure/macos.yml b/ci/azure/macos.yml new file mode 100644 index 0000000000000..25b66615dac7e --- /dev/null +++ b/ci/azure/macos.yml @@ -0,0 +1,39 @@ +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + maxParallel: 11 + matrix: + py35_np_110: + ENV_FILE: ci/azure-macos-35.yml + CONDA_PY: "35" + CONDA_ENV: pandas + TEST_ARGS: "--skip-slow --skip-network" + + steps: + - script: | + if [ "$(uname)" == "Linux" ]; then sudo apt-get install -y libc6-dev-i386; 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 + ci/incremental/build.sh + displayName: 'Build' + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + ci/script_single.sh + ci/script_multi.sh + echo "[Test done]" + displayName: 'Test' + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas && pushd /tmp && python -c "import pandas; pandas.show_versions();" && popd diff --git a/ci/azure/windows-py27.yml b/ci/azure/windows-py27.yml new file mode 100644 index 0000000000000..e60844896b71c --- /dev/null +++ b/ci/azure/windows-py27.yml @@ -0,0 +1,41 @@ +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + maxParallel: 11 + matrix: + py36_np14: + ENV_FILE: ci/azure-windows-27.yml + CONDA_PY: "27" + CONDA_ENV: pandas + + steps: + - task: CondaEnvironment@1 + inputs: + updateConda: no + packageSpecs: '' + + # Need to install VC 9.0 only for Python 2.7 + # Once we understand how to do tasks conditional on build matrix variables + # we could merge this into azure-windows.yml + - 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' + + - script: | + ci\\incremental\\setup_conda_environment.cmd + displayName: 'Before Install' + - script: | + ci\\incremental\\build.cmd + displayName: 'Build' + - script: | + call activate %CONDA_ENV% + pytest --skip-slow --skip-network pandas -n 2 -r sxX --strict %* + displayName: 'Test' diff --git a/ci/azure/windows.yml b/ci/azure/windows.yml new file mode 100644 index 0000000000000..6090139fb4f3e --- /dev/null +++ b/ci/azure/windows.yml @@ -0,0 +1,32 @@ +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + maxParallel: 11 + matrix: + py36_np14: + ENV_FILE: ci/azure-windows-36.yml + CONDA_PY: "36" + CONDA_ENV: pandas + + steps: + - task: CondaEnvironment@1 + inputs: + updateConda: no + packageSpecs: '' + + - script: | + ci\\incremental\\setup_conda_environment.cmd + displayName: 'Before Install' + - script: | + ci\\incremental\\build.cmd + displayName: 'Build' + - script: | + call activate %CONDA_ENV% + pytest --skip-slow --skip-network pandas -n 2 -r sxX --strict %* + displayName: 'Test' diff --git a/ci/incremental/build.cmd b/ci/incremental/build.cmd new file mode 100644 index 0000000000000..d2fd06d7d9e50 --- /dev/null +++ b/ci/incremental/build.cmd @@ -0,0 +1,10 @@ +@rem https://github.com/numba/numba/blob/master/buildscripts/incremental/build.cmd +call activate %CONDA_ENV% + +@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..8f2301a3b7ef5 --- /dev/null +++ b/ci/incremental/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +source activate $CONDA_ENV + +# 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..b4446c49fabd3 --- /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 %CONDA_ENV% +@rem Scipy, CFFI, jinja2 and IPython are optional dependencies, but exercised in the test suite +conda env create -n %CONDA_ENV% --file=ci\azure-windows-%CONDA_PY%.yaml + +call activate %CONDA_ENV% +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..c716a39138644 --- /dev/null +++ b/ci/incremental/setup_conda_environment.sh @@ -0,0 +1,48 @@ +#!/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 $CONDA_ENV + +echo +echo "[create env]" +time conda env create -q -n "${CONDA_ENV}" --file="${ENV_FILE}" || exit 1 + +# Activate first +set +v +source activate $CONDA_ENV +set -v + +# remove any installed pandas package +# w/o removing anything else +echo +echo "[removing installed pandas]" +conda remove pandas -y --force +pip uninstall -y pandas + +echo +echo "[no installed pandas]" +conda list pandas + +# # 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 From 40dfadd02fa564434323007240d48b57068eea25 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 19 Sep 2018 10:49:36 -0500 Subject: [PATCH 29/87] CI: Fix travis CI (#22765) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a180e83eeec21..40baee2c03ea0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ matrix: # Exclude the default Python 3.5 build - python: 3.5 + include: - dist: trusty env: - JOB="3.7" ENV_FILE="ci/travis-37.yaml" TEST_ARGS="--skip-slow --skip-network" From f87fe147c7494f3db56f3de31aeda12f80ef9c67 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 19 Sep 2018 13:31:10 -0500 Subject: [PATCH 30/87] CI: Publish test summary (#22770) --- ci/azure/macos.yml | 4 ++++ ci/azure/windows-py27.yml | 6 +++++- ci/azure/windows.yml | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ci/azure/macos.yml b/ci/azure/macos.yml index 25b66615dac7e..5bf8d18d6cbb9 100644 --- a/ci/azure/macos.yml +++ b/ci/azure/macos.yml @@ -37,3 +37,7 @@ jobs: - script: | export PATH=$HOME/miniconda3/bin:$PATH source activate pandas && pushd /tmp && python -c "import pandas; pandas.show_versions();" && popd + - task: PublishTestResults@2 + inputs: + testResultsFiles: '/tmp/*.xml' + testRunTitle: 'MacOS-35' diff --git a/ci/azure/windows-py27.yml b/ci/azure/windows-py27.yml index e60844896b71c..3e92c96263930 100644 --- a/ci/azure/windows-py27.yml +++ b/ci/azure/windows-py27.yml @@ -37,5 +37,9 @@ jobs: displayName: 'Build' - script: | call activate %CONDA_ENV% - pytest --skip-slow --skip-network pandas -n 2 -r sxX --strict %* + pytest --junitxml=test-data.xml --skip-slow --skip-network pandas -n 2 -r sxX --strict %* displayName: 'Test' + - task: PublishTestResults@2 + inputs: + testResultsFiles: 'test-data.xml' + testRunTitle: 'Windows 27' diff --git a/ci/azure/windows.yml b/ci/azure/windows.yml index 6090139fb4f3e..2ab8c6f320188 100644 --- a/ci/azure/windows.yml +++ b/ci/azure/windows.yml @@ -28,5 +28,9 @@ jobs: displayName: 'Build' - script: | call activate %CONDA_ENV% - pytest --skip-slow --skip-network pandas -n 2 -r sxX --strict %* + pytest --junitxml=test-data.xml --skip-slow --skip-network pandas -n 2 -r sxX --strict %* displayName: 'Test' + - task: PublishTestResults@2 + inputs: + testResultsFiles: 'test-data.xml' + testRunTitle: 'Windows 36' From 1c113db60b68c5a262d64e92dc9de72bfe59aed5 Mon Sep 17 00:00:00 2001 From: Yeojin Kim <38222260+yeojin-dev@users.noreply.github.com> Date: Thu, 20 Sep 2018 06:17:12 +0900 Subject: [PATCH 31/87] BUG: Check types in Index.__contains__ (#22085) (#22602) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/indexes/numeric.py | 23 +++++++++++++++++++++-- pandas/tests/indexing/test_indexing.py | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 487d5d0d2accd..9e2c20c78f489 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -723,6 +723,7 @@ Indexing - ``Float64Index.get_loc`` now raises ``KeyError`` when boolean key passed. (:issue:`19087`) - Bug in :meth:`DataFrame.loc` when indexing with an :class:`IntervalIndex` (:issue:`19977`) - :class:`Index` no longer mangles ``None``, ``NaN`` and ``NaT``, i.e. they are treated as three different keys. However, for numeric Index all three are still coerced to a ``NaN`` (:issue:`22332`) +- Bug in `scalar in Index` if scalar is a float while the ``Index`` is of integer dtype (:issue:`22085`) Missing ^^^^^^^ diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index 8d616468a87d9..7f64fb744c682 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -6,6 +6,7 @@ pandas_dtype, needs_i8_conversion, is_integer_dtype, + is_float, is_bool, is_bool_dtype, is_scalar) @@ -162,7 +163,25 @@ def insert(self, loc, item): ) -class Int64Index(NumericIndex): +class IntegerIndex(NumericIndex): + """ + This is an abstract class for Int64Index, UInt64Index. + """ + + def __contains__(self, key): + """ + Check if key is a float and has a decimal. If it has, return False. + """ + hash(key) + try: + if is_float(key) and int(key) != key: + return False + return key in self._engine + except (OverflowError, TypeError, ValueError): + return False + + +class Int64Index(IntegerIndex): __doc__ = _num_index_shared_docs['class_descr'] % _int64_descr_args _typ = 'int64index' @@ -220,7 +239,7 @@ def _assert_safe_casting(cls, data, subarr): ) -class UInt64Index(NumericIndex): +class UInt64Index(IntegerIndex): __doc__ = _num_index_shared_docs['class_descr'] % _uint64_descr_args _typ = 'uint64index' diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 33b7c1b8154c7..761c633f89da3 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -631,6 +631,21 @@ def test_mixed_index_not_contains(self, index, val): # GH 19860 assert val not in index + def test_contains_with_float_index(self): + # GH#22085 + integer_index = pd.Int64Index([0, 1, 2, 3]) + uinteger_index = pd.UInt64Index([0, 1, 2, 3]) + float_index = pd.Float64Index([0.1, 1.1, 2.2, 3.3]) + + for index in (integer_index, uinteger_index): + assert 1.1 not in index + assert 1.0 in index + assert 1 in index + + assert 1.1 in float_index + assert 1.0 not in float_index + assert 1 not in float_index + def test_index_type_coercion(self): with catch_warnings(record=True): From 117d0b1011c090b4658b0e84c2b572ee713e21de Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 20 Sep 2018 09:40:10 -0400 Subject: [PATCH 32/87] BUG: Empty CategoricalIndex fails with boolean categories (#22710) * TST: Add failing test for empty bool Categoricals * BUG: Failure in empty boolean CategoricalIndex Fixes GH #22702. --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/arrays/categorical.py | 8 ++++++-- pandas/tests/arrays/categorical/test_constructors.py | 6 ++++++ pandas/tests/indexes/test_category.py | 6 ++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 9e2c20c78f489..e2ba35c1ad7f9 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -616,6 +616,7 @@ Categorical ^^^^^^^^^^^ - Bug in :meth:`Categorical.from_codes` where ``NaN`` values in ``codes`` were silently converted to ``0`` (:issue:`21767`). In the future this will raise a ``ValueError``. Also changes the behavior of ``.from_codes([1.1, 2.0])``. +- Constructing a :class:`pd.CategoricalIndex` with empty values and boolean categories was raising a ``ValueError`` after a change to dtype coercion (:issue:`22702`). Datetimelike ^^^^^^^^^^^^ diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 63a1dacb47abb..216bccf7d6309 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -2439,9 +2439,13 @@ def _get_codes_for_values(values, categories): """ utility routine to turn values into codes given the specified categories """ - from pandas.core.algorithms import _get_data_algo, _hashtables - if not is_dtype_equal(values.dtype, categories.dtype): + if is_dtype_equal(values.dtype, categories.dtype): + # To prevent erroneous dtype coercion in _get_data_algo, retrieve + # the underlying numpy array. gh-22702 + values = getattr(values, 'values', values) + categories = getattr(categories, 'values', categories) + else: values = ensure_object(values) categories = ensure_object(categories) diff --git a/pandas/tests/arrays/categorical/test_constructors.py b/pandas/tests/arrays/categorical/test_constructors.py index b5f499ba27323..998c1182c013a 100644 --- a/pandas/tests/arrays/categorical/test_constructors.py +++ b/pandas/tests/arrays/categorical/test_constructors.py @@ -42,6 +42,12 @@ def test_constructor_empty(self): expected = pd.Int64Index([1, 2, 3]) tm.assert_index_equal(c.categories, expected) + def test_constructor_empty_boolean(self): + # see gh-22702 + cat = pd.Categorical([], categories=[True, False]) + categories = sorted(cat.categories.tolist()) + assert categories == [False, True] + def test_constructor_tuples(self): values = np.array([(1,), (1, 2), (1,), (1, 2)], dtype=object) result = Categorical(values) diff --git a/pandas/tests/indexes/test_category.py b/pandas/tests/indexes/test_category.py index 2221fd023b561..d49a6a6abc7c9 100644 --- a/pandas/tests/indexes/test_category.py +++ b/pandas/tests/indexes/test_category.py @@ -136,6 +136,12 @@ def test_construction_with_dtype(self): result = CategoricalIndex(idx, categories=idx, ordered=True) tm.assert_index_equal(result, expected, exact=True) + def test_construction_empty_with_bool_categories(self): + # see gh-22702 + cat = pd.CategoricalIndex([], categories=[True, False]) + categories = sorted(cat.categories.tolist()) + assert categories == [False, True] + def test_construction_with_categorical_dtype(self): # construction with CategoricalDtype # GH18109 From e568fb090a6a5f3a03fc3beb451ffb1e7115dba4 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 20 Sep 2018 09:01:31 -0500 Subject: [PATCH 33/87] is_bool_dtype for ExtensionArrays (#22667) Closes https://github.com/pandas-dev/pandas/issues/22665 Closes https://github.com/pandas-dev/pandas/issues/22326 --- doc/source/whatsnew/v0.24.0.txt | 4 +- pandas/core/common.py | 40 ++++++- pandas/core/dtypes/base.py | 20 ++++ pandas/core/dtypes/common.py | 17 +++ pandas/core/dtypes/dtypes.py | 6 + .../tests/arrays/categorical/test_indexing.py | 27 ++++- pandas/tests/dtypes/test_dtypes.py | 14 ++- pandas/tests/extension/arrow/__init__.py | 0 pandas/tests/extension/arrow/bool.py | 108 ++++++++++++++++++ pandas/tests/extension/arrow/test_bool.py | 48 ++++++++ 10 files changed, 276 insertions(+), 8 deletions(-) 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 diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index e2ba35c1ad7f9..2f70d4e5946a0 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -485,6 +485,7 @@ ExtensionType Changes - ``ExtensionArray`` has gained the abstract methods ``.dropna()`` (:issue:`21185`) - ``ExtensionDtype`` has gained the ability to instantiate from string dtypes, e.g. ``decimal`` would instantiate a registered ``DecimalDtype``; furthermore the ``ExtensionDtype`` has gained the method ``construct_array_type`` (:issue:`21185`) +- An ``ExtensionArray`` with a boolean dtype now works correctly as a boolean indexer. :meth:`pandas.api.types.is_bool_dtype` now properly considers them boolean (:issue:`22326`) - Added ``ExtensionDtype._is_numeric`` for controlling whether an extension dtype is considered numeric (:issue:`22290`). - The ``ExtensionArray`` constructor, ``_from_sequence`` now take the keyword arg ``copy=False`` (:issue:`21185`) - Bug in :meth:`Series.get` for ``Series`` using ``ExtensionArray`` and integer index (:issue:`21257`) @@ -616,7 +617,8 @@ Categorical ^^^^^^^^^^^ - Bug in :meth:`Categorical.from_codes` where ``NaN`` values in ``codes`` were silently converted to ``0`` (:issue:`21767`). In the future this will raise a ``ValueError``. Also changes the behavior of ``.from_codes([1.1, 2.0])``. -- Constructing a :class:`pd.CategoricalIndex` with empty values and boolean categories was raising a ``ValueError`` after a change to dtype coercion (:issue:`22702`). +- Bug when indexing with a boolean-valued ``Categorical``. Now a boolean-valued ``Categorical`` is treated as a boolean mask (:issue:`22665`) +- Constructing a :class:`CategoricalIndex` with empty values and boolean categories was raising a ``ValueError`` after a change to dtype coercion (:issue:`22702`). Datetimelike ^^^^^^^^^^^^ diff --git a/pandas/core/common.py b/pandas/core/common.py index a6b05daf1d85d..14e47936e1b50 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -15,7 +15,9 @@ from pandas import compat from pandas.compat import iteritems, PY36, OrderedDict from pandas.core.dtypes.generic import ABCSeries, ABCIndex, ABCIndexClass -from pandas.core.dtypes.common import is_integer +from pandas.core.dtypes.common import ( + is_integer, is_bool_dtype, is_extension_array_dtype, is_array_like +) from pandas.core.dtypes.inference import _iterable_not_string from pandas.core.dtypes.missing import isna, isnull, notnull # noqa from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike @@ -100,17 +102,45 @@ def maybe_box_datetimelike(value): def is_bool_indexer(key): - if isinstance(key, (ABCSeries, np.ndarray, ABCIndex)): + # type: (Any) -> bool + """ + Check whether `key` is a valid boolean indexer. + + Parameters + ---------- + key : Any + Only list-likes may be considered boolean indexers. + All other types are not considered a boolean indexer. + For array-like input, boolean ndarrays or ExtensionArrays + with ``_is_boolean`` set are considered boolean indexers. + + Returns + ------- + bool + + Raises + ------ + ValueError + When the array is an object-dtype ndarray or ExtensionArray + and contains missing values. + """ + na_msg = 'cannot index with vector containing NA / NaN values' + if (isinstance(key, (ABCSeries, np.ndarray, ABCIndex)) or + (is_array_like(key) and is_extension_array_dtype(key.dtype))): if key.dtype == np.object_: key = np.asarray(values_from_object(key)) if not lib.is_bool_array(key): if isna(key).any(): - raise ValueError('cannot index with vector containing ' - 'NA / NaN values') + raise ValueError(na_msg) return False return True - elif key.dtype == np.bool_: + elif is_bool_dtype(key.dtype): + # an ndarray with bool-dtype by definition has no missing values. + # So we only need to check for NAs in ExtensionArrays + if is_extension_array_dtype(key.dtype): + if np.any(key.isna()): + raise ValueError(na_msg) return True elif isinstance(key, list): try: diff --git a/pandas/core/dtypes/base.py b/pandas/core/dtypes/base.py index 7dcdf878231f1..a552251ebbafa 100644 --- a/pandas/core/dtypes/base.py +++ b/pandas/core/dtypes/base.py @@ -106,6 +106,25 @@ def _is_numeric(self): """ return False + @property + def _is_boolean(self): + # type: () -> bool + """ + Whether this dtype should be considered boolean. + + By default, ExtensionDtypes are assumed to be non-numeric. + Setting this to True will affect the behavior of several places, + e.g. + + * is_bool + * boolean indexing + + Returns + ------- + bool + """ + return False + class ExtensionDtype(_DtypeOpsMixin): """A custom data type, to be paired with an ExtensionArray. @@ -125,6 +144,7 @@ class ExtensionDtype(_DtypeOpsMixin): pandas operations * _is_numeric + * _is_boolean Optionally one can override construct_array_type for construction with the name of this dtype via the Registry. See diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index f6e7e87f1043b..e2b9e246aee50 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1619,6 +1619,11 @@ def is_bool_dtype(arr_or_dtype): ------- boolean : Whether or not the array or dtype is of a boolean dtype. + Notes + ----- + An ExtensionArray is considered boolean when the ``_is_boolean`` + attribute is set to True. + Examples -------- >>> is_bool_dtype(str) @@ -1635,6 +1640,8 @@ def is_bool_dtype(arr_or_dtype): False >>> is_bool_dtype(np.array([True, False])) True + >>> is_bool_dtype(pd.Categorical([True, False])) + True """ if arr_or_dtype is None: @@ -1645,6 +1652,13 @@ def is_bool_dtype(arr_or_dtype): # this isn't even a dtype return False + if isinstance(arr_or_dtype, (ABCCategorical, ABCCategoricalIndex)): + arr_or_dtype = arr_or_dtype.dtype + + if isinstance(arr_or_dtype, CategoricalDtype): + arr_or_dtype = arr_or_dtype.categories + # now we use the special definition for Index + if isinstance(arr_or_dtype, ABCIndexClass): # TODO(jreback) @@ -1653,6 +1667,9 @@ def is_bool_dtype(arr_or_dtype): # guess this return (arr_or_dtype.is_object and arr_or_dtype.inferred_type == 'boolean') + elif is_extension_array_dtype(arr_or_dtype): + dtype = getattr(arr_or_dtype, 'dtype', arr_or_dtype) + return dtype._is_boolean return issubclass(tipo, np.bool_) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 4fd77e41a1c67..d879ded4f0f09 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -462,6 +462,12 @@ def ordered(self): """Whether the categories have an ordered relationship""" return self._ordered + @property + def _is_boolean(self): + from pandas.core.dtypes.common import is_bool_dtype + + return is_bool_dtype(self.categories) + class DatetimeTZDtypeType(type): """ diff --git a/pandas/tests/arrays/categorical/test_indexing.py b/pandas/tests/arrays/categorical/test_indexing.py index b54ac2835bee3..d23da1565a952 100644 --- a/pandas/tests/arrays/categorical/test_indexing.py +++ b/pandas/tests/arrays/categorical/test_indexing.py @@ -5,7 +5,8 @@ import numpy as np import pandas.util.testing as tm -from pandas import Categorical, Index, CategoricalIndex, PeriodIndex +from pandas import Categorical, Index, CategoricalIndex, PeriodIndex, Series +import pandas.core.common as com from pandas.tests.arrays.categorical.common import TestCategorical @@ -121,3 +122,27 @@ def test_get_indexer_non_unique(self, idx_values, key_values, key_class): tm.assert_numpy_array_equal(expected, result) tm.assert_numpy_array_equal(exp_miss, res_miss) + + +@pytest.mark.parametrize("index", [True, False]) +def test_mask_with_boolean(index): + s = Series(range(3)) + idx = Categorical([True, False, True]) + if index: + idx = CategoricalIndex(idx) + + assert com.is_bool_indexer(idx) + result = s[idx] + expected = s[idx.astype('object')] + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("index", [True, False]) +def test_mask_with_boolean_raises(index): + s = Series(range(3)) + idx = Categorical([True, False, None]) + if index: + idx = CategoricalIndex(idx) + + with tm.assert_raises_regex(ValueError, 'NA / NaN'): + s[idx] diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 55c841ba1fc46..e3d14497a38f9 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -17,7 +17,7 @@ is_dtype_equal, is_datetime64_ns_dtype, is_datetime64_dtype, is_interval_dtype, is_datetime64_any_dtype, is_string_dtype, - _coerce_to_dtype) + _coerce_to_dtype, is_bool_dtype) import pandas.util.testing as tm @@ -126,6 +126,18 @@ def test_tuple_categories(self): result = CategoricalDtype(categories) assert all(result.categories == categories) + @pytest.mark.parametrize("categories, expected", [ + ([True, False], True), + ([True, False, None], True), + ([True, False, "a", "b'"], False), + ([0, 1], False), + ]) + def test_is_boolean(self, categories, expected): + cat = Categorical(categories) + assert cat.dtype._is_boolean is expected + assert is_bool_dtype(cat) is expected + assert is_bool_dtype(cat.dtype) is expected + class TestDatetimeTZDtype(Base): diff --git a/pandas/tests/extension/arrow/__init__.py b/pandas/tests/extension/arrow/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/extension/arrow/bool.py b/pandas/tests/extension/arrow/bool.py new file mode 100644 index 0000000000000..a9da25cdd2755 --- /dev/null +++ b/pandas/tests/extension/arrow/bool.py @@ -0,0 +1,108 @@ +"""Rudimentary Apache Arrow-backed ExtensionArray. + +At the moment, just a boolean array / type is implemented. +Eventually, we'll want to parametrize the type and support +multiple dtypes. Not all methods are implemented yet, and the +current implementation is not efficient. +""" +import copy +import itertools + +import numpy as np +import pyarrow as pa +import pandas as pd +from pandas.api.extensions import ( + ExtensionDtype, ExtensionArray, take, register_extension_dtype +) + + +@register_extension_dtype +class ArrowBoolDtype(ExtensionDtype): + + type = np.bool_ + kind = 'b' + name = 'arrow_bool' + na_value = pa.NULL + + @classmethod + def construct_from_string(cls, string): + if string == cls.name: + return cls() + else: + raise TypeError("Cannot construct a '{}' from " + "'{}'".format(cls, string)) + + @classmethod + def construct_array_type(cls): + return ArrowBoolArray + + def _is_boolean(self): + return True + + +class ArrowBoolArray(ExtensionArray): + def __init__(self, values): + if not isinstance(values, pa.ChunkedArray): + raise ValueError + + assert values.type == pa.bool_() + self._data = values + self._dtype = ArrowBoolDtype() + + def __repr__(self): + return "ArrowBoolArray({})".format(repr(self._data)) + + @classmethod + def from_scalars(cls, values): + arr = pa.chunked_array([pa.array(np.asarray(values))]) + return cls(arr) + + @classmethod + def from_array(cls, arr): + assert isinstance(arr, pa.Array) + return cls(pa.chunked_array([arr])) + + @classmethod + def _from_sequence(cls, scalars, dtype=None, copy=False): + return cls.from_scalars(scalars) + + def __getitem__(self, item): + return self._data.to_pandas()[item] + + def __len__(self): + return len(self._data) + + @property + def dtype(self): + return self._dtype + + @property + def nbytes(self): + return sum(x.size for chunk in self._data.chunks + for x in chunk.buffers() + if x is not None) + + def isna(self): + return pd.isna(self._data.to_pandas()) + + def take(self, indices, allow_fill=False, fill_value=None): + data = self._data.to_pandas() + + if allow_fill and fill_value is None: + fill_value = self.dtype.na_value + + result = take(data, indices, fill_value=fill_value, + allow_fill=allow_fill) + return self._from_sequence(result, dtype=self.dtype) + + def copy(self, deep=False): + if deep: + return copy.deepcopy(self._data) + else: + return copy.copy(self._data) + + def _concat_same_type(cls, to_concat): + chunks = list(itertools.chain.from_iterable(x._data.chunks + for x in to_concat)) + arr = pa.chunked_array(chunks) + return cls(arr) diff --git a/pandas/tests/extension/arrow/test_bool.py b/pandas/tests/extension/arrow/test_bool.py new file mode 100644 index 0000000000000..e1afedcade3ff --- /dev/null +++ b/pandas/tests/extension/arrow/test_bool.py @@ -0,0 +1,48 @@ +import numpy as np +import pytest +import pandas as pd +import pandas.util.testing as tm +from pandas.tests.extension import base + +pytest.importorskip('pyarrow', minversion="0.10.0") + +from .bool import ArrowBoolDtype, ArrowBoolArray + + +@pytest.fixture +def dtype(): + return ArrowBoolDtype() + + +@pytest.fixture +def data(): + return ArrowBoolArray.from_scalars(np.random.randint(0, 2, size=100, + dtype=bool)) + + +class BaseArrowTests(object): + pass + + +class TestDtype(BaseArrowTests, base.BaseDtypeTests): + def test_array_type_with_arg(self, data, dtype): + pytest.skip("GH-22666") + + +class TestInterface(BaseArrowTests, base.BaseInterfaceTests): + def test_repr(self, data): + raise pytest.skip("TODO") + + +class TestConstructors(BaseArrowTests, base.BaseConstructorsTests): + def test_from_dtype(self, data): + pytest.skip("GH-22666") + + +def test_is_bool_dtype(data): + assert pd.api.types.is_bool_dtype(data) + assert pd.core.common.is_bool_indexer(data) + s = pd.Series(range(len(data))) + result = s[data] + expected = s[np.asarray(data)] + tm.assert_series_equal(result, expected) From 793b24e2051b2bab2d35b2a07fb7ae3306cba5b5 Mon Sep 17 00:00:00 2001 From: Diego Argueta Date: Thu, 20 Sep 2018 07:31:14 -0700 Subject: [PATCH 34/87] DOC: Fix outdated default values in util.testing docstrings (#22776) --- pandas/util/testing.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pandas/util/testing.py b/pandas/util/testing.py index edd0b0aa82d23..3db251e89842d 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -225,7 +225,7 @@ def assert_almost_equal(left, right, check_dtype="equiv", ---------- left : object right : object - check_dtype : bool / string {'equiv'}, default False + 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. @@ -787,7 +787,7 @@ 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 substituted for Int64Index as well. @@ -1034,7 +1034,7 @@ def assert_interval_array_equal(left, right, exact='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 'Categorical' + obj : str, default 'IntervalArray' Specify object name being compared, internally used to show appropriate assertion message """ @@ -1326,12 +1326,13 @@ def assert_frame_equal(left, right, check_dtype=True, Second DataFrame to compare. check_dtype : bool, default True Whether to check the DataFrame dtype is identical. - check_index_type : {'equiv'} or bool, default 'equiv' + check_index_type : bool / string {'equiv'}, default 'equiv' Whether to check the Index class, dtype and inferred_type are identical. - check_column_type : {'equiv'} or bool, default 'equiv' + check_column_type : bool / string {'equiv'}, default 'equiv' Whether to check the columns class, dtype and inferred_type - are identical. + 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 From 32a74f19b655f4b0ef198d5ff4e15d409c3670d2 Mon Sep 17 00:00:00 2001 From: James Bourbeau Date: Thu, 20 Sep 2018 09:52:52 -0500 Subject: [PATCH 35/87] DOC: Reorders DataFrame.any and all docstrings (#22774) --- pandas/core/generic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 3f7334131e146..75baeab402734 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9671,15 +9671,15 @@ def _doc_parms(cls): original index. * None : reduce all axes, return a scalar. +bool_only : boolean, default None + Include only boolean columns. If None, will attempt to use everything, + then use only boolean data. Not implemented for Series. skipna : boolean, default True Exclude NA/null values. If an entire row/column is NA, the result will be NA. level : int or level name, default None If the axis is a MultiIndex (hierarchical), count along a particular level, collapsing into a %(name1)s. -bool_only : boolean, default None - Include only boolean columns. If None, will attempt to use everything, - then use only boolean data. Not implemented for Series. **kwargs : any, default None Additional keywords have no effect but might be accepted for compatibility with NumPy. From 0480f4c183a95712cb8ceaf5682c5b8dd02e0f21 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 20 Sep 2018 11:22:45 -0500 Subject: [PATCH 36/87] ENH: _is_homogeneous (#22780) --- pandas/core/base.py | 15 +++++++++++++ pandas/core/frame.py | 28 ++++++++++++++++++++++++ pandas/core/indexes/multi.py | 20 +++++++++++++++++ pandas/tests/frame/test_dtypes.py | 24 ++++++++++++++++++++ pandas/tests/indexing/test_multiindex.py | 8 +++++++ pandas/tests/series/test_dtypes.py | 5 +++++ 6 files changed, 100 insertions(+) diff --git a/pandas/core/base.py b/pandas/core/base.py index d831dc69338bd..26fea89b45ae1 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -663,6 +663,21 @@ def transpose(self, *args, **kwargs): T = property(transpose, doc="return the transpose, which is by " "definition self") + @property + def _is_homogeneous(self): + """Whether the object has a single dtype. + + By definition, Series and Index are always considered homogeneous. + A MultiIndex may or may not be homogeneous, depending on the + dtypes of the levels. + + See Also + -------- + DataFrame._is_homogeneous + MultiIndex._is_homogeneous + """ + return True + @property def shape(self): """ return a tuple of the shape of the underlying data """ diff --git a/pandas/core/frame.py b/pandas/core/frame.py index bb221ced9e6bd..959b0a4fd1890 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -613,6 +613,34 @@ def shape(self): """ return len(self.index), len(self.columns) + @property + def _is_homogeneous(self): + """ + Whether all the columns in a DataFrame have the same type. + + Returns + ------- + bool + + Examples + -------- + >>> DataFrame({"A": [1, 2], "B": [3, 4]})._is_homogeneous + True + >>> DataFrame({"A": [1, 2], "B": [3.0, 4.0]})._is_homogeneous + False + + Items with the same type but different sizes are considered + different types. + + >>> DataFrame({"A": np.array([1, 2], dtype=np.int32), + ... "B": np.array([1, 2], dtype=np.int64)})._is_homogeneous + False + """ + if self._data.any_extension_types: + return len({block.dtype for block in self._data.blocks}) == 1 + else: + return not self._data.is_mixed_type + def _repr_fits_vertical_(self): """ Check length against max_rows. diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index a7932f667f6de..ad38f037b6578 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -288,6 +288,26 @@ def _verify_integrity(self, labels=None, levels=None): def levels(self): return self._levels + @property + def _is_homogeneous(self): + """Whether the levels of a MultiIndex all have the same dtype. + + This looks at the dtypes of the levels. + + See Also + -------- + Index._is_homogeneous + DataFrame._is_homogeneous + + Examples + -------- + >>> MultiIndex.from_tuples([('a', 'b'), ('a', 'c')])._is_homogeneous + True + >>> MultiIndex.from_tuples([('a', 1), ('a', 2)])._is_homogeneous + False + """ + return len({x.dtype for x in self.levels}) <= 1 + def _set_levels(self, levels, level=None, copy=False, validate=True, verify_integrity=False): # This is NOT part of the levels property because it should be diff --git a/pandas/tests/frame/test_dtypes.py b/pandas/tests/frame/test_dtypes.py index 3b3ab3d03dce9..ca4bd64659e06 100644 --- a/pandas/tests/frame/test_dtypes.py +++ b/pandas/tests/frame/test_dtypes.py @@ -815,6 +815,30 @@ def test_constructor_list_str_na(self, string_dtype): expected = DataFrame({"A": ['1.0', '2.0', None]}, dtype=object) assert_frame_equal(result, expected) + @pytest.mark.parametrize("data, expected", [ + # empty + (DataFrame(), True), + # multi-same + (DataFrame({"A": [1, 2], "B": [1, 2]}), True), + # multi-object + (DataFrame({"A": np.array([1, 2], dtype=object), + "B": np.array(["a", "b"], dtype=object)}), True), + # multi-extension + (DataFrame({"A": pd.Categorical(['a', 'b']), + "B": pd.Categorical(['a', 'b'])}), True), + # differ types + (DataFrame({"A": [1, 2], "B": [1., 2.]}), False), + # differ sizes + (DataFrame({"A": np.array([1, 2], dtype=np.int32), + "B": np.array([1, 2], dtype=np.int64)}), False), + # multi-extension differ + (DataFrame({"A": pd.Categorical(['a', 'b']), + "B": pd.Categorical(['b', 'c'])}), False), + + ]) + def test_is_homogeneous(self, data, expected): + assert data._is_homogeneous is expected + class TestDataFrameDatetimeWithTZ(TestData): diff --git a/pandas/tests/indexing/test_multiindex.py b/pandas/tests/indexing/test_multiindex.py index 9e66dfad3ddc7..aefa8badf72e7 100644 --- a/pandas/tests/indexing/test_multiindex.py +++ b/pandas/tests/indexing/test_multiindex.py @@ -733,6 +733,14 @@ def test_multiindex_contains_dropped(self): assert 'a' in idx.levels[0] assert 'a' not in idx + @pytest.mark.parametrize("data, expected", [ + (MultiIndex.from_product([(), ()]), True), + (MultiIndex.from_product([(1, 2), (3, 4)]), True), + (MultiIndex.from_product([('a', 'b'), (1, 2)]), False), + ]) + def test_multiindex_is_homogeneous(self, data, expected): + assert data._is_homogeneous is expected + class TestMultiIndexSlicers(object): diff --git a/pandas/tests/series/test_dtypes.py b/pandas/tests/series/test_dtypes.py index 7aecaf340a3e0..83a458eedbd93 100644 --- a/pandas/tests/series/test_dtypes.py +++ b/pandas/tests/series/test_dtypes.py @@ -508,3 +508,8 @@ def test_infer_objects_series(self): assert actual.dtype == 'object' tm.assert_series_equal(actual, expected) + + def test_is_homogeneous(self): + assert Series()._is_homogeneous + assert Series([1, 2])._is_homogeneous + assert Series(pd.Categorical([1, 2]))._is_homogeneous From 8a1164ce10fafbafad2b2b5ea4e608b29d677e91 Mon Sep 17 00:00:00 2001 From: alimcmaster1 Date: Thu, 20 Sep 2018 22:21:09 +0100 Subject: [PATCH 37/87] Enforce E741 (#22795) --- .pep8speaks.yml | 1 - setup.cfg | 1 - 2 files changed, 2 deletions(-) diff --git a/.pep8speaks.yml b/.pep8speaks.yml index fda26d87bf7f6..cd610907007eb 100644 --- a/.pep8speaks.yml +++ b/.pep8speaks.yml @@ -8,5 +8,4 @@ pycodestyle: ignore: # Errors and warnings to ignore - E402, # module level import not at top of file - E731, # do not assign a lambda expression, use a def - - E741, # do not use variables named 'l', 'O', or 'I' - W503 # line break before binary operator diff --git a/setup.cfg b/setup.cfg index fb42dfd3b6d15..e4a2357def474 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ parentdir_prefix = pandas- ignore = E402, # module level import not at top of file E731, # do not assign a lambda expression, use a def - E741, # do not use variables named 'l', 'O', or 'I' W503, # line break before binary operator C405, # Unnecessary (list/tuple) literal - rewrite as a set literal. C406, # Unnecessary (list/tuple) literal - rewrite as a dict literal. From f8c5705f0bf0063acec0ff45b10404f38893a5fa Mon Sep 17 00:00:00 2001 From: dannyhyunkim <38394262+dannyhyunkim@users.noreply.github.com> Date: Fri, 21 Sep 2018 08:17:20 +1000 Subject: [PATCH 38/87] ENH: Making header_style a property of ExcelFormatter #22758 (#22759) --- pandas/io/formats/excel.py | 45 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 0bc268bc18b95..d6fcfb2207cf9 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -34,15 +34,6 @@ def __init__(self, row, col, val, style=None, mergestart=None, self.mergeend = mergeend -header_style = {"font": {"bold": True}, - "borders": {"top": "thin", - "right": "thin", - "bottom": "thin", - "left": "thin"}, - "alignment": {"horizontal": "center", - "vertical": "top"}} - - class CSSToExcelConverter(object): """A callable for converting CSS declarations to ExcelWriter styles @@ -389,6 +380,16 @@ def __init__(self, df, na_rep='', float_format=None, cols=None, self.merge_cells = merge_cells self.inf_rep = inf_rep + @property + def header_style(self): + return {"font": {"bold": True}, + "borders": {"top": "thin", + "right": "thin", + "bottom": "thin", + "left": "thin"}, + "alignment": {"horizontal": "center", + "vertical": "top"}} + def _format_value(self, val): if is_scalar(val) and missing.isna(val): val = self.na_rep @@ -427,7 +428,7 @@ def _format_header_mi(self): # Format multi-index as a merged cells. for lnum in range(len(level_lengths)): name = columns.names[lnum] - yield ExcelCell(lnum, coloffset, name, header_style) + yield ExcelCell(lnum, coloffset, name, self.header_style) for lnum, (spans, levels, labels) in enumerate(zip( level_lengths, columns.levels, columns.labels)): @@ -435,16 +436,16 @@ def _format_header_mi(self): for i in spans: if spans[i] > 1: yield ExcelCell(lnum, coloffset + i + 1, values[i], - header_style, lnum, + self.header_style, lnum, coloffset + i + spans[i]) else: yield ExcelCell(lnum, coloffset + i + 1, values[i], - header_style) + self.header_style) else: # Format in legacy format with dots to indicate levels. for i, values in enumerate(zip(*level_strs)): v = ".".join(map(pprint_thing, values)) - yield ExcelCell(lnum, coloffset + i + 1, v, header_style) + yield ExcelCell(lnum, coloffset + i + 1, v, self.header_style) self.rowcounter = lnum @@ -469,7 +470,7 @@ def _format_header_regular(self): for colindex, colname in enumerate(colnames): yield ExcelCell(self.rowcounter, colindex + coloffset, colname, - header_style) + self.header_style) def _format_header(self): if isinstance(self.columns, ABCMultiIndex): @@ -482,7 +483,8 @@ def _format_header(self): row = [x if x is not None else '' for x in self.df.index.names] + [''] * len(self.columns) if reduce(lambda x, y: x and y, map(lambda x: x != '', row)): - gen2 = (ExcelCell(self.rowcounter, colindex, val, header_style) + gen2 = (ExcelCell(self.rowcounter, colindex, val, + self.header_style) for colindex, val in enumerate(row)) self.rowcounter += 1 return itertools.chain(gen, gen2) @@ -518,7 +520,7 @@ def _format_regular_rows(self): if index_label and self.header is not False: yield ExcelCell(self.rowcounter - 1, 0, index_label, - header_style) + self.header_style) # write index_values index_values = self.df.index @@ -526,7 +528,8 @@ def _format_regular_rows(self): index_values = self.df.index.to_timestamp() for idx, idxval in enumerate(index_values): - yield ExcelCell(self.rowcounter + idx, 0, idxval, header_style) + yield ExcelCell(self.rowcounter + idx, 0, idxval, + self.header_style) coloffset = 1 else: @@ -562,7 +565,7 @@ def _format_hierarchical_rows(self): for cidx, name in enumerate(index_labels): yield ExcelCell(self.rowcounter - 1, cidx, name, - header_style) + self.header_style) if self.merge_cells: # Format hierarchical rows as merged cells. @@ -581,12 +584,12 @@ def _format_hierarchical_rows(self): for i in spans: if spans[i] > 1: yield ExcelCell(self.rowcounter + i, gcolidx, - values[i], header_style, + values[i], self.header_style, self.rowcounter + i + spans[i] - 1, gcolidx) else: yield ExcelCell(self.rowcounter + i, gcolidx, - values[i], header_style) + values[i], self.header_style) gcolidx += 1 else: @@ -594,7 +597,7 @@ def _format_hierarchical_rows(self): for indexcolvals in zip(*self.df.index): for idx, indexcolval in enumerate(indexcolvals): yield ExcelCell(self.rowcounter + idx, gcolidx, - indexcolval, header_style) + indexcolval, self.header_style) gcolidx += 1 for cell in self._generate_body(gcolidx): From 4612a828244725dab1ff928b71cf92d04b40cd04 Mon Sep 17 00:00:00 2001 From: Armin Varshokar Date: Thu, 20 Sep 2018 18:18:30 -0400 Subject: [PATCH 39/87] API/DEPR: 'periods' argument instead of 'n' for DatetimeIndex.shift() (#22697) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/arrays/datetimelike.py | 39 +++++++++++++++------- pandas/core/generic.py | 5 +++ pandas/tests/indexes/datetimes/test_ops.py | 10 ++++++ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 2f70d4e5946a0..135e97f309d7e 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -562,6 +562,7 @@ Deprecations - :meth:`Series.str.cat` has deprecated using arbitrary list-likes *within* list-likes. A list-like container may still contain many ``Series``, ``Index`` or 1-dimensional ``np.ndarray``, or alternatively, only scalar values. (:issue:`21950`) - :meth:`FrozenNDArray.searchsorted` has deprecated the ``v`` parameter in favor of ``value`` (:issue:`14645`) +- :func:`DatetimeIndex.shift` now accepts ``periods`` argument instead of ``n`` for consistency with :func:`Index.shift` and :func:`Series.shift`. Using ``n`` throws a deprecation warning (:issue:`22458`) .. _whatsnew_0240.prior_deprecations: diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 69925ce1c520e..91c119808db52 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -38,6 +38,7 @@ from pandas.core.algorithms import checked_add_with_arr from .base import ExtensionOpsMixin +from pandas.util._decorators import deprecate_kwarg def _make_comparison_op(op, cls): @@ -522,40 +523,54 @@ def _addsub_offset_array(self, other, op): kwargs['freq'] = 'infer' return type(self)(res_values, **kwargs) - def shift(self, n, freq=None): + @deprecate_kwarg(old_arg_name='n', new_arg_name='periods') + def shift(self, periods, freq=None): """ - Specialized shift which produces a Datetime/Timedelta Array/Index + Shift index by desired number of time frequency increments. + + This method is for shifting the values of datetime-like indexes + by a specified time increment a given number of times. Parameters ---------- - n : int - Periods to shift by - freq : DateOffset or timedelta-like, optional + periods : int + Number of periods (or increments) to shift by, + can be positive or negative. + + .. versionchanged:: 0.24.0 + + freq : pandas.DateOffset, pandas.Timedelta or string, optional + Frequency increment to shift by. + If None, the index is shifted by its own `freq` attribute. + Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc. Returns ------- - shifted : same type as self + pandas.DatetimeIndex + Shifted index. + + See Also + -------- + Index.shift : Shift values of Index. """ if freq is not None and freq != self.freq: if isinstance(freq, compat.string_types): freq = frequencies.to_offset(freq) - offset = n * freq + offset = periods * freq result = self + offset - if hasattr(self, 'tz'): result._tz = self.tz - return result - if n == 0: + if periods == 0: # immutable so OK return self.copy() if self.freq is None: raise NullFrequencyError("Cannot shift with no freq") - start = self[0] + n * self.freq - end = self[-1] + n * self.freq + start = self[0] + periods * self.freq + end = self[-1] + periods * self.freq attribs = self._get_attributes_dict() return self._generate_range(start=start, end=end, periods=None, **attribs) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 75baeab402734..b72d8cbf02bc6 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -8288,6 +8288,11 @@ def mask(self, cond, other=np.nan, inplace=False, axis=None, level=None, See Notes. axis : %(axes_single_arg)s + See Also + -------- + Index.shift : Shift values of Index. + DatetimeIndex.shift : Shift values of DatetimeIndex. + Notes ----- If freq is specified then the index values are shifted but the data diff --git a/pandas/tests/indexes/datetimes/test_ops.py b/pandas/tests/indexes/datetimes/test_ops.py index 24d99abaf44a8..b60b222d095b9 100644 --- a/pandas/tests/indexes/datetimes/test_ops.py +++ b/pandas/tests/indexes/datetimes/test_ops.py @@ -540,6 +540,16 @@ def test_shift(self): shifted = rng.shift(1, freq=CDay()) assert shifted[0] == rng[0] + CDay() + def test_shift_periods(self): + # GH #22458 : argument 'n' was deprecated in favor of 'periods' + idx = pd.DatetimeIndex(start=START, end=END, + periods=3) + tm.assert_index_equal(idx.shift(periods=0), idx) + tm.assert_index_equal(idx.shift(0), idx) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=True): + tm.assert_index_equal(idx.shift(n=0), idx) + def test_pickle_unpickle(self): unpickled = tm.round_trip_pickle(self.rng) assert unpickled.freq is not None From bdb7a1603f1e0948ca0cab011987f616e7296167 Mon Sep 17 00:00:00 2001 From: Diego Argueta Date: Fri, 21 Sep 2018 01:17:59 -0700 Subject: [PATCH 40/87] ENH: Add support for excluding the index from Parquet files (GH20768) (#22266) --- doc/source/io.rst | 38 +++++++++++++++++++++++++++++++++ doc/source/whatsnew/v0.24.0.txt | 4 ++++ pandas/core/frame.py | 11 ++++++++-- pandas/io/parquet.py | 33 ++++++++++++++++++++-------- pandas/tests/io/test_parquet.py | 34 +++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/doc/source/io.rst b/doc/source/io.rst index c2c8c1c17700f..cb22bb9198e25 100644 --- a/doc/source/io.rst +++ b/doc/source/io.rst @@ -4570,6 +4570,9 @@ dtypes, including extension dtypes such as datetime with tz. Several caveats. * Duplicate column names and non-string columns names are not supported. +* The ``pyarrow`` engine always writes the index to the output, but ``fastparquet`` only writes non-default + indexes. This extra column can cause problems for non-Pandas consumers that are not expecting it. You can + force including or omitting indexes with the ``index`` argument, regardless of the underlying engine. * Index level names, if specified, must be strings. * Categorical dtypes can be serialized to parquet, but will de-serialize as ``object`` dtype. * Non supported types include ``Period`` and actual Python object types. These will raise a helpful error message @@ -4633,6 +4636,41 @@ Read only certain columns of a parquet file. os.remove('example_pa.parquet') os.remove('example_fp.parquet') + +Handling Indexes +'''''''''''''''' + +Serializing a ``DataFrame`` to parquet may include the implicit index as one or +more columns in the output file. Thus, this code: + +.. ipython:: python + + df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]}) + df.to_parquet('test.parquet', engine='pyarrow') + +creates a parquet file with *three* columns if you use ``pyarrow`` for serialization: +``a``, ``b``, and ``__index_level_0__``. If you're using ``fastparquet``, the +index `may or may not `_ +be written to the file. + +This unexpected extra column causes some databases like Amazon Redshift to reject +the file, because that column doesn't exist in the target table. + +If you want to omit a dataframe's indexes when writing, pass ``index=False`` to +:func:`~pandas.DataFrame.to_parquet`: + +.. ipython:: python + + df.to_parquet('test.parquet', index=False) + +This creates a parquet file with just the two expected columns, ``a`` and ``b``. +If your ``DataFrame`` has a custom index, you won't get it back when you load +this file into a ``DataFrame``. + +Passing ``index=True`` will *always* write the index, even if that's not the +underlying engine's default behavior. + + .. _io.sql: SQL Queries diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 135e97f309d7e..ed1bf0a4f8394 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -17,6 +17,10 @@ New features - ``ExcelWriter`` now accepts ``mode`` as a keyword argument, enabling append to existing workbooks when using the ``openpyxl`` engine (:issue:`3441`) +- :func:`DataFrame.to_parquet` now accepts ``index`` as an argument, allowing +the user to override the engine's default behavior to include or omit the +dataframe's indexes from the resulting Parquet file. (:issue:`20768`) + .. _whatsnew_0240.enhancements.extension_array_operators: ``ExtensionArray`` operator support diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 959b0a4fd1890..0099f705fe1e1 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -1902,7 +1902,7 @@ def to_feather(self, fname): to_feather(self, fname) def to_parquet(self, fname, engine='auto', compression='snappy', - **kwargs): + index=None, **kwargs): """ Write a DataFrame to the binary parquet format. @@ -1924,6 +1924,13 @@ def to_parquet(self, fname, engine='auto', compression='snappy', 'pyarrow' is unavailable. compression : {'snappy', 'gzip', 'brotli', None}, default 'snappy' Name of the compression to use. Use ``None`` for no compression. + index : bool, default None + If ``True``, include the dataframe's index(es) in the file output. + If ``False``, they will not be written to the file. If ``None``, + the behavior depends on the chosen engine. + + .. versionadded:: 0.24.0 + **kwargs Additional arguments passed to the parquet library. See :ref:`pandas io ` for more details. @@ -1952,7 +1959,7 @@ def to_parquet(self, fname, engine='auto', compression='snappy', """ from pandas.io.parquet import to_parquet to_parquet(self, fname, engine, - compression=compression, **kwargs) + compression=compression, index=index, **kwargs) @Substitution(header='Write out the column names. If a list of strings ' 'is given, it is assumed to be aliases for the ' diff --git a/pandas/io/parquet.py b/pandas/io/parquet.py index a99014f07a6b3..6ab56c68a510a 100644 --- a/pandas/io/parquet.py +++ b/pandas/io/parquet.py @@ -103,19 +103,27 @@ def __init__(self): self.api = pyarrow def write(self, df, path, compression='snappy', - coerce_timestamps='ms', **kwargs): + coerce_timestamps='ms', index=None, **kwargs): self.validate_dataframe(df) - if self._pyarrow_lt_070: + + # Only validate the index if we're writing it. + if self._pyarrow_lt_070 and index is not False: self._validate_write_lt_070(df) path, _, _, _ = get_filepath_or_buffer(path, mode='wb') + if index is None: + from_pandas_kwargs = {} + else: + from_pandas_kwargs = {'preserve_index': index} + if self._pyarrow_lt_060: - table = self.api.Table.from_pandas(df, timestamps_to_ms=True) + table = self.api.Table.from_pandas(df, timestamps_to_ms=True, + **from_pandas_kwargs) self.api.parquet.write_table( table, path, compression=compression, **kwargs) else: - table = self.api.Table.from_pandas(df) + table = self.api.Table.from_pandas(df, **from_pandas_kwargs) self.api.parquet.write_table( table, path, compression=compression, coerce_timestamps=coerce_timestamps, **kwargs) @@ -197,7 +205,7 @@ def __init__(self): ) self.api = fastparquet - def write(self, df, path, compression='snappy', **kwargs): + def write(self, df, path, compression='snappy', index=None, **kwargs): self.validate_dataframe(df) # thriftpy/protocol/compact.py:339: # DeprecationWarning: tostring() is deprecated. @@ -214,8 +222,8 @@ def write(self, df, path, compression='snappy', **kwargs): path, _, _, _ = get_filepath_or_buffer(path) with catch_warnings(record=True): - self.api.write(path, df, - compression=compression, **kwargs) + self.api.write(path, df, compression=compression, + write_index=index, **kwargs) def read(self, path, columns=None, **kwargs): if is_s3_url(path): @@ -234,7 +242,8 @@ def read(self, path, columns=None, **kwargs): return parquet_file.to_pandas(columns=columns, **kwargs) -def to_parquet(df, path, engine='auto', compression='snappy', **kwargs): +def to_parquet(df, path, engine='auto', compression='snappy', index=None, + **kwargs): """ Write a DataFrame to the parquet format. @@ -250,11 +259,17 @@ def to_parquet(df, path, engine='auto', compression='snappy', **kwargs): 'pyarrow' is unavailable. compression : {'snappy', 'gzip', 'brotli', None}, default 'snappy' Name of the compression to use. Use ``None`` for no compression. + index : bool, default None + If ``True``, include the dataframe's index(es) in the file output. If + ``False``, they will not be written to the file. If ``None``, the + engine's default behavior will be used. + + .. versionadded 0.24.0 kwargs Additional keyword arguments passed to the engine """ impl = get_engine(engine) - return impl.write(df, path, compression=compression, **kwargs) + return impl.write(df, path, compression=compression, index=index, **kwargs) def read_parquet(path, engine='auto', columns=None, **kwargs): diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index fefbe8afb59cb..ab7f04ad86ffc 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -368,6 +368,40 @@ def test_multiindex_with_columns(self, pa_ge_070): 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): From fb784caf51a37ffd9ec3662ade1f5e9cdebefd17 Mon Sep 17 00:00:00 2001 From: aeltanawy Date: Sat, 22 Sep 2018 16:36:22 -0700 Subject: [PATCH 41/87] DOC: Updated the DataFrame.assign docstring (#21917) --- ci/doctests.sh | 2 +- pandas/core/frame.py | 70 ++++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/ci/doctests.sh b/ci/doctests.sh index e7fe80e60eb6d..48774a1e4d00d 100755 --- a/ci/doctests.sh +++ b/ci/doctests.sh @@ -21,7 +21,7 @@ if [ "$DOCTEST" ]; then # DataFrame / Series docstrings pytest --doctest-modules -v pandas/core/frame.py \ - -k"-assign -axes -combine -isin -itertuples -join -nlargest -nsmallest -nunique -pivot_table -quantile -query -reindex -reindex_axis -replace -round -set_index -stack -to_dict -to_stata" + -k"-axes -combine -isin -itertuples -join -nlargest -nsmallest -nunique -pivot_table -quantile -query -reindex -reindex_axis -replace -round -set_index -stack -to_dict -to_stata" if [ $? -ne "0" ]; then RET=1 diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 0099f705fe1e1..81d5c112885ec 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -3280,7 +3280,7 @@ def assign(self, **kwargs): Parameters ---------- - kwargs : keyword, value pairs + **kwargs : dict of {str: callable or Series} The column names are keywords. If the values are callable, they are computed on the DataFrame and assigned to the new columns. The callable must not @@ -3290,7 +3290,7 @@ def assign(self, **kwargs): Returns ------- - df : DataFrame + DataFrame A new DataFrame with the new columns in addition to all the existing columns. @@ -3310,48 +3310,34 @@ def assign(self, **kwargs): Examples -------- - >>> df = pd.DataFrame({'A': range(1, 11), 'B': np.random.randn(10)}) + >>> df = pd.DataFrame({'temp_c': [17.0, 25.0]}, + ... index=['Portland', 'Berkeley']) + >>> df + temp_c + Portland 17.0 + Berkeley 25.0 Where the value is a callable, evaluated on `df`: - - >>> df.assign(ln_A = lambda x: np.log(x.A)) - A B ln_A - 0 1 0.426905 0.000000 - 1 2 -0.780949 0.693147 - 2 3 -0.418711 1.098612 - 3 4 -0.269708 1.386294 - 4 5 -0.274002 1.609438 - 5 6 -0.500792 1.791759 - 6 7 1.649697 1.945910 - 7 8 -1.495604 2.079442 - 8 9 0.549296 2.197225 - 9 10 -0.758542 2.302585 - - Where the value already exists and is inserted: - - >>> newcol = np.log(df['A']) - >>> df.assign(ln_A=newcol) - A B ln_A - 0 1 0.426905 0.000000 - 1 2 -0.780949 0.693147 - 2 3 -0.418711 1.098612 - 3 4 -0.269708 1.386294 - 4 5 -0.274002 1.609438 - 5 6 -0.500792 1.791759 - 6 7 1.649697 1.945910 - 7 8 -1.495604 2.079442 - 8 9 0.549296 2.197225 - 9 10 -0.758542 2.302585 - - Where the keyword arguments depend on each other - - >>> df = pd.DataFrame({'A': [1, 2, 3]}) - - >>> df.assign(B=df.A, C=lambda x:x['A']+ x['B']) - A B C - 0 1 1 2 - 1 2 2 4 - 2 3 3 6 + >>> df.assign(temp_f=lambda x: x.temp_c * 9 / 5 + 32) + temp_c temp_f + Portland 17.0 62.6 + Berkeley 25.0 77.0 + + Alternatively, the same behavior can be achieved by directly + referencing an existing Series or sequence: + >>> df.assign(temp_f=df['temp_c'] * 9 / 5 + 32) + temp_c temp_f + Portland 17.0 62.6 + Berkeley 25.0 77.0 + + In Python 3.6+, you can create multiple columns within the same assign + where one of the columns depends on another one defined within the same + assign: + >>> df.assign(temp_f=lambda x: x['temp_c'] * 9 / 5 + 32, + ... temp_k=lambda x: (x['temp_f'] + 459.67) * 5 / 9) + temp_c temp_f temp_k + Portland 17.0 62.6 290.15 + Berkeley 25.0 77.0 298.15 """ data = self.copy() From f65fa755db23c8010d60f2160c522264417cb545 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Sun, 23 Sep 2018 05:11:01 -0700 Subject: [PATCH 42/87] BUG: Avoid AmbiguousTime or NonExistentTime Error when resampling (#22809) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/resample.py | 36 +++++++++++++++------------------ pandas/tests/test_resample.py | 16 +++++++++++++++ 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index ed1bf0a4f8394..31ef70703e2ca 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -679,6 +679,7 @@ Timezones - Bug when setting a new value with :meth:`DataFrame.loc` with a :class:`DatetimeIndex` with a DST transition (:issue:`18308`, :issue:`20724`) - Bug in :meth:`DatetimeIndex.unique` that did not re-localize tz-aware dates correctly (:issue:`21737`) - Bug when indexing a :class:`Series` with a DST transition (:issue:`21846`) +- Bug in :meth:`DataFrame.resample` and :meth:`Series.resample` where an ``AmbiguousTimeError`` or ``NonExistentTimeError`` would raise if a timezone aware timeseries ended on a DST transition (:issue:`19375`, :issue:`10117`) Offsets ^^^^^^^ diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 1ef8a0854887b..878ac957a8557 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1328,8 +1328,7 @@ def _get_time_bins(self, ax): 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, + first, last = _get_range_edges(ax.min(), ax.max(), self.freq, closed=self.closed, base=self.base) tz = ax.tz @@ -1519,9 +1518,6 @@ def _take_new_index(obj, indexer, new_index, axis=0): 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)) @@ -1531,8 +1527,7 @@ def _get_range_edges(first, last, offset, closed='left', base=0): return _adjust_dates_anchored(first, last, offset, closed=closed, base=base) - if not isinstance(offset, Tick): # and first.time() != last.time(): - # hack! + else: first = first.normalize() last = last.normalize() @@ -1553,19 +1548,16 @@ def _adjust_dates_anchored(first, last, offset, closed='right', base=0): # # 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. + # GH 10117 & GH 19375. If first and last contain timezone information, + # Perform the calculation in UTC in order to avoid localizing on an + # Ambiguous or Nonexistent time. 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 + if first_tzinfo is not None: + first = first.tz_convert('UTC') + if last_tzinfo is not None: + last = last.tz_convert('UTC') base_nanos = (base % offset.n) * offset.nanos // offset.n start_day_nanos += base_nanos @@ -1598,9 +1590,13 @@ def _adjust_dates_anchored(first, last, offset, closed='right', base=0): 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)) + fresult = Timestamp(fresult) + lresult = Timestamp(lresult) + if first_tzinfo is not None: + fresult = fresult.tz_localize('UTC').tz_convert(first_tzinfo) + if last_tzinfo is not None: + lresult = lresult.tz_localize('UTC').tz_convert(last_tzinfo) + return fresult, lresult def asfreq(obj, freq, method=None, how=None, normalize=False, fill_value=None): diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index 377253574d2c1..ccd2461d1512e 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -2485,6 +2485,22 @@ def test_with_local_timezone_dateutil(self): 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) + + 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'), From 945bf75103f3465a5d937a661a0baf9bc156db6b Mon Sep 17 00:00:00 2001 From: Anjali2019 Date: Sun, 23 Sep 2018 14:37:41 +0200 Subject: [PATCH 43/87] TST: Fixturize series/test_asof.py (#22772) --- pandas/tests/series/test_asof.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/tests/series/test_asof.py b/pandas/tests/series/test_asof.py index 3104d85601434..e85a0ac42ae1a 100644 --- a/pandas/tests/series/test_asof.py +++ b/pandas/tests/series/test_asof.py @@ -8,10 +8,8 @@ import pandas.util.testing as tm -from .common import TestData - -class TestSeriesAsof(TestData): +class TestSeriesAsof(): def test_basic(self): From f67b90ded3c55a641e1e3d8afb007811f69da372 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Sun, 23 Sep 2018 06:25:41 -0700 Subject: [PATCH 44/87] BUG/ENH: Handle AmbiguousTimeError in date rounding (#22647) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/_libs/tslibs/nattype.pyx | 29 ++++++ pandas/_libs/tslibs/timestamps.pyx | 45 +++++++-- pandas/core/indexes/datetimelike.py | 35 +++++-- .../tests/scalar/timestamp/test_unary_ops.py | 22 +++++ pandas/tests/series/test_datetime_values.py | 95 ++++++++++++------- 6 files changed, 173 insertions(+), 54 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 31ef70703e2ca..28d0c33851ae4 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -186,6 +186,7 @@ Other Enhancements - :func:`to_timedelta` now supports iso-formated timedelta strings (:issue:`21877`) - :class:`Series` and :class:`DataFrame` now support :class:`Iterable` in constructor (:issue:`2193`) - :class:`DatetimeIndex` gained :attr:`DatetimeIndex.timetz` attribute. Returns local time with timezone information. (:issue:`21358`) +- :meth:`round`, :meth:`ceil`, and meth:`floor` for :class:`DatetimeIndex` and :class:`Timestamp` now support an ``ambiguous`` argument for handling datetimes that are rounded to ambiguous times (:issue:`18946`) - :class:`Resampler` now is iterable like :class:`GroupBy` (:issue:`15314`). - :meth:`Series.resample` and :meth:`DataFrame.resample` have gained the :meth:`Resampler.quantile` (:issue:`15023`). - :meth:`Index.to_frame` now supports overriding column name(s) (:issue:`22580`). diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index fd8486f690745..ae4f9c821b5d1 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -477,6 +477,13 @@ class NaTType(_NaT): Parameters ---------- freq : a freq string indicating the rounding resolution + ambiguous : bool, 'NaT', default 'raise' + - bool contains flags to determine if time is dst or not (note + that this flag is only applicable for ambiguous fall dst dates) + - 'NaT' will return NaT for an ambiguous time + - 'raise' will raise an AmbiguousTimeError for an ambiguous time + + .. versionadded:: 0.24.0 Raises ------ @@ -489,6 +496,17 @@ class NaTType(_NaT): Parameters ---------- freq : a freq string indicating the flooring resolution + ambiguous : bool, 'NaT', default 'raise' + - bool contains flags to determine if time is dst or not (note + that this flag is only applicable for ambiguous fall dst dates) + - 'NaT' will return NaT for an ambiguous time + - 'raise' will raise an AmbiguousTimeError for an ambiguous time + + .. versionadded:: 0.24.0 + + Raises + ------ + ValueError if the freq cannot be converted """) ceil = _make_nat_func('ceil', # noqa:E128 """ @@ -497,6 +515,17 @@ class NaTType(_NaT): Parameters ---------- freq : a freq string indicating the ceiling resolution + ambiguous : bool, 'NaT', default 'raise' + - bool contains flags to determine if time is dst or not (note + that this flag is only applicable for ambiguous fall dst dates) + - 'NaT' will return NaT for an ambiguous time + - 'raise' will raise an AmbiguousTimeError for an ambiguous time + + .. versionadded:: 0.24.0 + + Raises + ------ + ValueError if the freq cannot be converted """) tz_convert = _make_nat_func('tz_convert', # noqa:E128 diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 52343593d1cc1..e985a519c3046 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -656,7 +656,7 @@ class Timestamp(_Timestamp): return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq) - def _round(self, freq, rounder): + def _round(self, freq, rounder, ambiguous='raise'): if self.tz is not None: value = self.tz_localize(None).value else: @@ -668,10 +668,10 @@ class Timestamp(_Timestamp): r = round_ns(value, rounder, freq)[0] result = Timestamp(r, unit='ns') if self.tz is not None: - result = result.tz_localize(self.tz) + result = result.tz_localize(self.tz, ambiguous=ambiguous) return result - def round(self, freq): + def round(self, freq, ambiguous='raise'): """ Round the Timestamp to the specified resolution @@ -682,32 +682,61 @@ class Timestamp(_Timestamp): Parameters ---------- freq : a freq string indicating the rounding resolution + ambiguous : bool, 'NaT', default 'raise' + - bool contains flags to determine if time is dst or not (note + that this flag is only applicable for ambiguous fall dst dates) + - 'NaT' will return NaT for an ambiguous time + - 'raise' will raise an AmbiguousTimeError for an ambiguous time + + .. versionadded:: 0.24.0 Raises ------ ValueError if the freq cannot be converted """ - return self._round(freq, np.round) + return self._round(freq, np.round, ambiguous) - def floor(self, freq): + def floor(self, freq, ambiguous='raise'): """ return a new Timestamp floored to this resolution Parameters ---------- freq : a freq string indicating the flooring resolution + ambiguous : bool, 'NaT', default 'raise' + - bool contains flags to determine if time is dst or not (note + that this flag is only applicable for ambiguous fall dst dates) + - 'NaT' will return NaT for an ambiguous time + - 'raise' will raise an AmbiguousTimeError for an ambiguous time + + .. versionadded:: 0.24.0 + + Raises + ------ + ValueError if the freq cannot be converted """ - return self._round(freq, np.floor) + return self._round(freq, np.floor, ambiguous) - def ceil(self, freq): + def ceil(self, freq, ambiguous='raise'): """ return a new Timestamp ceiled to this resolution Parameters ---------- freq : a freq string indicating the ceiling resolution + ambiguous : bool, 'NaT', default 'raise' + - bool contains flags to determine if time is dst or not (note + that this flag is only applicable for ambiguous fall dst dates) + - 'NaT' will return NaT for an ambiguous time + - 'raise' will raise an AmbiguousTimeError for an ambiguous time + + .. versionadded:: 0.24.0 + + Raises + ------ + ValueError if the freq cannot be converted """ - return self._round(freq, np.ceil) + return self._round(freq, np.ceil, ambiguous) @property def tz(self): diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 3f8c07fe7cd21..578167a7db500 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -99,6 +99,18 @@ class TimelikeOps(object): frequency like 'S' (second) not 'ME' (month end). See :ref:`frequency aliases ` for a list of possible `freq` values. + 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 designates + 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 + Only relevant for DatetimeIndex + + .. versionadded:: 0.24.0 Returns ------- @@ -168,7 +180,7 @@ class TimelikeOps(object): """ ) - def _round(self, freq, rounder): + def _round(self, freq, rounder, ambiguous): # round the local times values = _ensure_datetimelike_to_i8(self) result = round_ns(values, rounder, freq) @@ -180,19 +192,20 @@ def _round(self, freq, rounder): if 'tz' in attribs: attribs['tz'] = None return self._ensure_localized( - self._shallow_copy(result, **attribs)) + self._shallow_copy(result, **attribs), ambiguous + ) @Appender((_round_doc + _round_example).format(op="round")) - def round(self, freq, *args, **kwargs): - return self._round(freq, np.round) + def round(self, freq, ambiguous='raise'): + return self._round(freq, np.round, ambiguous) @Appender((_round_doc + _floor_example).format(op="floor")) - def floor(self, freq): - return self._round(freq, np.floor) + def floor(self, freq, ambiguous='raise'): + return self._round(freq, np.floor, ambiguous) @Appender((_round_doc + _ceil_example).format(op="ceil")) - def ceil(self, freq): - return self._round(freq, np.ceil) + def ceil(self, freq, ambiguous='raise'): + return self._round(freq, np.ceil, ambiguous) class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin): @@ -264,7 +277,7 @@ def _evaluate_compare(self, other, op): except TypeError: return result - def _ensure_localized(self, result): + def _ensure_localized(self, result, ambiguous='raise'): """ ensure that we are re-localized @@ -274,6 +287,8 @@ def _ensure_localized(self, result): Parameters ---------- result : DatetimeIndex / i8 ndarray + ambiguous : str, bool, or bool-ndarray + default 'raise' Returns ------- @@ -284,7 +299,7 @@ def _ensure_localized(self, result): if getattr(self, 'tz', None) is not None: if not isinstance(result, ABCIndexClass): result = self._simple_new(result) - result = result.tz_localize(self.tz) + result = result.tz_localize(self.tz, ambiguous=ambiguous) return result def _box_values_as_index(self): diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index bf41840c58ded..f83aa31edf95a 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -132,6 +132,28 @@ def test_floor(self): expected = Timestamp('20130101') assert result == expected + @pytest.mark.parametrize('method', ['ceil', 'round', 'floor']) + def test_round_dst_border(self, method): + # GH 18946 round near 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') + # -------------------------------------------------------------- # Timestamp.replace diff --git a/pandas/tests/series/test_datetime_values.py b/pandas/tests/series/test_datetime_values.py index 5b45c6003a005..fee2323310b9c 100644 --- a/pandas/tests/series/test_datetime_values.py +++ b/pandas/tests/series/test_datetime_values.py @@ -5,6 +5,7 @@ import calendar import unicodedata import pytest +import pytz from datetime import datetime, time, date @@ -95,42 +96,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') @@ -261,6 +226,64 @@ def get_dir(s): with pytest.raises(com.SettingWithCopyError): s.dt.hour[0] = 5 + @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 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') + def test_dt_namespace_accessor_categorical(self): # GH 19468 dti = DatetimeIndex(['20171111', '20181212']).repeat(2) From e5a99c65fb98deaab5ced62cc25cabd4970bcfc4 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 23 Sep 2018 06:35:16 -0700 Subject: [PATCH 45/87] Fix Series v Index bool ops (#22173) --- doc/source/whatsnew/v0.24.0.txt | 2 - pandas/core/ops.py | 58 ++++++++++++++----------- pandas/tests/series/test_operators.py | 61 ++++++++++++++++----------- 3 files changed, 70 insertions(+), 51 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 28d0c33851ae4..618d7454c67fe 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -819,5 +819,3 @@ Other - :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly. - Logical operations ``&, |, ^`` between :class:`Series` and :class:`Index` will no longer raise ``ValueError`` (:issue:`22092`) - -- -- diff --git a/pandas/core/ops.py b/pandas/core/ops.py index a7fc2839ea101..70fe7de0a973e 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -1525,23 +1525,22 @@ def _bool_method_SERIES(cls, op, special): Wrapper function for Series arithmetic operations, to avoid code duplication. """ + op_name = _get_op_name(op, special) def na_op(x, y): try: result = op(x, y) except TypeError: - if isinstance(y, list): - y = construct_1d_object_array_from_listlike(y) - - if isinstance(y, (np.ndarray, ABCSeries, ABCIndexClass)): - if (is_bool_dtype(x.dtype) and is_bool_dtype(y.dtype)): - result = op(x, y) # when would this be hit? - else: - x = ensure_object(x) - y = ensure_object(y) - result = libops.vec_binop(x, y, op) + assert not isinstance(y, (list, ABCSeries, ABCIndexClass)) + if isinstance(y, np.ndarray): + # bool-bool dtype operations should be OK, should not get here + assert not (is_bool_dtype(x) and is_bool_dtype(y)) + x = ensure_object(x) + y = ensure_object(y) + result = libops.vec_binop(x, y, op) else: # let null fall thru + assert lib.is_scalar(y) if not isna(y): y = bool(y) try: @@ -1561,33 +1560,42 @@ def wrapper(self, other): is_self_int_dtype = is_integer_dtype(self.dtype) self, other = _align_method_SERIES(self, other, align_asobject=True) + res_name = get_op_result_name(self, other) if isinstance(other, ABCDataFrame): # Defer to DataFrame implementation; fail early return NotImplemented - elif isinstance(other, ABCSeries): - name = get_op_result_name(self, other) + elif isinstance(other, (ABCSeries, ABCIndexClass)): is_other_int_dtype = is_integer_dtype(other.dtype) other = fill_int(other) if is_other_int_dtype else fill_bool(other) - filler = (fill_int if is_self_int_dtype and is_other_int_dtype - else fill_bool) - - res_values = na_op(self.values, other.values) - unfilled = self._constructor(res_values, - index=self.index, name=name) - return filler(unfilled) + ovalues = other.values + finalizer = lambda x: x else: # scalars, list, tuple, np.array - filler = (fill_int if is_self_int_dtype and - is_integer_dtype(np.asarray(other)) else fill_bool) - - res_values = na_op(self.values, other) - unfilled = self._constructor(res_values, index=self.index) - return filler(unfilled).__finalize__(self) + is_other_int_dtype = is_integer_dtype(np.asarray(other)) + if is_list_like(other) and not isinstance(other, np.ndarray): + # TODO: Can we do this before the is_integer_dtype check? + # could the is_integer_dtype check be checking the wrong + # thing? e.g. other = [[0, 1], [2, 3], [4, 5]]? + other = construct_1d_object_array_from_listlike(other) + + ovalues = other + finalizer = lambda x: x.__finalize__(self) + + # For int vs int `^`, `|`, `&` are bitwise operators and return + # integer dtypes. Otherwise these are boolean ops + filler = (fill_int if is_self_int_dtype and is_other_int_dtype + else fill_bool) + res_values = na_op(self.values, ovalues) + unfilled = self._constructor(res_values, + index=self.index, name=res_name) + filled = filler(unfilled) + return finalizer(filled) + wrapper.__name__ = op_name return wrapper diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 615f0c9247bd8..601e251d45b4b 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -14,6 +14,7 @@ NaT, date_range, timedelta_range, Categorical) from pandas.core.indexes.datetimes import Timestamp import pandas.core.nanops as nanops +from pandas.core import ops from pandas.compat import range from pandas import compat @@ -425,30 +426,6 @@ 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_logical_ops_with_index(self): - # GH22092 - ser = Series([True, True, False, False]) - idx1 = Index([True, False, True, False]) - idx2 = Index([1, 0, 1, 0]) - - expected = Series([True, False, False, False]) - result1 = ser & idx1 - assert_series_equal(result1, expected) - result2 = ser & idx2 - assert_series_equal(result2, expected) - - expected = Series([True, True, True, False]) - result1 = ser | idx1 - assert_series_equal(result1, expected) - result2 = ser | idx2 - assert_series_equal(result2, expected) - - expected = Series([False, True, True, False]) - result1 = ser ^ idx1 - assert_series_equal(result1, expected) - result2 = ser ^ idx2 - assert_series_equal(result2, expected) - def test_ne(self): ts = Series([3, 4, 5, 6, 7], [3, 4, 5, 6, 7], dtype=float) expected = [True, True, False, True, True] @@ -627,6 +604,42 @@ def test_ops_datetimelike_align(self): result = (dt2.to_frame() - dt.to_frame())[0] assert_series_equal(result, expected) + @pytest.mark.parametrize('op', [ + operator.and_, + operator.or_, + operator.xor, + pytest.param(ops.rand_, + marks=pytest.mark.xfail(reason="GH#22092 Index " + "implementation returns " + "Index", + raises=AssertionError, + strict=True)), + pytest.param(ops.ror_, + marks=pytest.mark.xfail(reason="GH#22092 Index " + "implementation raises", + raises=ValueError, strict=True)), + pytest.param(ops.rxor, + marks=pytest.mark.xfail(reason="GH#22092 Index " + "implementation raises", + raises=TypeError, strict=True)) + ]) + def test_bool_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]) + + expected = Series([op(ser[n], idx1[n]) for n in range(len(ser))]) + + result = op(ser, idx1) + assert_series_equal(result, expected) + + expected = Series([op(ser[n], idx2[n]) for n in range(len(ser))], + dtype=bool) + + result = op(ser, idx2) + assert_series_equal(result, expected) + def test_operators_bitwise(self): # GH 9016: support bitwise op for integer types index = list('bca') From 27de8e692f7bf3c2efdd9c2d49ec589c29fd74be Mon Sep 17 00:00:00 2001 From: Anjali2019 Date: Sun, 23 Sep 2018 15:42:14 +0200 Subject: [PATCH 46/87] TST: Fixturize series/test_apply.py (#22769) --- pandas/tests/series/test_apply.py | 99 ++++++++++++++++--------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/pandas/tests/series/test_apply.py b/pandas/tests/series/test_apply.py index b717d75d835d0..20215279cf031 100644 --- a/pandas/tests/series/test_apply.py +++ b/pandas/tests/series/test_apply.py @@ -17,18 +17,18 @@ import pandas.util.testing as tm from pandas.conftest import _get_cython_table_params -from .common import TestData +class TestSeriesApply(): -class TestSeriesApply(TestData): - - def test_apply(self): + def test_apply(self, datetime_series): with np.errstate(all='ignore'): - tm.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)) # element-wise apply import math - tm.assert_series_equal(self.ts.apply(math.exp), np.exp(self.ts)) + 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')) @@ -66,11 +66,11 @@ def test_apply_dont_convert_dtype(self): result = s.apply(f, convert_dtype=False) assert result.dtype == object - def test_with_string_args(self): + def test_with_string_args(self, datetime_series): for arg in ['sum', 'mean', 'min', 'max', 'std']: - result = self.ts.apply(arg) - expected = getattr(self.ts, arg)() + result = datetime_series.apply(arg) + expected = getattr(datetime_series, arg)() assert result == expected def test_apply_args(self): @@ -165,34 +165,34 @@ def test_apply_dict_depr(self): tsdf.A.agg({'foo': ['sum', 'mean']}) -class TestSeriesAggregate(TestData): +class TestSeriesAggregate(): - def test_transform(self): + def test_transform(self, string_series): # transforming functions with np.errstate(all='ignore'): - f_sqrt = np.sqrt(self.series) - f_abs = np.abs(self.series) + f_sqrt = np.sqrt(string_series) + f_abs = np.abs(string_series) # ufunc - result = self.series.transform(np.sqrt) + result = string_series.transform(np.sqrt) expected = f_sqrt.copy() assert_series_equal(result, expected) - result = self.series.apply(np.sqrt) + result = string_series.apply(np.sqrt) assert_series_equal(result, expected) # list-like - result = self.series.transform([np.sqrt]) + result = string_series.transform([np.sqrt]) expected = f_sqrt.to_frame().copy() expected.columns = ['sqrt'] assert_frame_equal(result, expected) - result = self.series.transform([np.sqrt]) + result = string_series.transform([np.sqrt]) assert_frame_equal(result, expected) - result = self.series.transform(['sqrt']) + result = string_series.transform(['sqrt']) assert_frame_equal(result, expected) # multiple items in list @@ -200,10 +200,10 @@ def test_transform(self): # series and then concatting expected = pd.concat([f_sqrt, f_abs], axis=1) expected.columns = ['sqrt', 'absolute'] - result = self.series.apply([np.sqrt, np.abs]) + result = string_series.apply([np.sqrt, np.abs]) assert_frame_equal(result, expected) - result = self.series.transform(['sqrt', 'abs']) + result = string_series.transform(['sqrt', 'abs']) expected.columns = ['sqrt', 'abs'] assert_frame_equal(result, expected) @@ -212,28 +212,28 @@ def test_transform(self): expected.columns = ['foo', 'bar'] expected = expected.unstack().rename('series') - result = self.series.apply({'foo': np.sqrt, 'bar': np.abs}) + 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): + def test_transform_and_agg_error(self, string_series): # we are trying to transform with an aggregator def f(): - self.series.transform(['min', 'max']) + string_series.transform(['min', 'max']) pytest.raises(ValueError, f) def f(): with np.errstate(all='ignore'): - self.series.agg(['sqrt', 'max']) + string_series.agg(['sqrt', 'max']) pytest.raises(ValueError, f) def f(): with np.errstate(all='ignore'): - self.series.transform(['sqrt', 'max']) + string_series.transform(['sqrt', 'max']) pytest.raises(ValueError, f) def f(): with np.errstate(all='ignore'): - self.series.agg({'foo': np.sqrt, 'bar': 'sum'}) + string_series.agg({'foo': np.sqrt, 'bar': 'sum'}) pytest.raises(ValueError, f) def test_demo(self): @@ -272,33 +272,34 @@ def test_multiple_aggregators_with_dict_api(self): 'min', 'sum']).unstack().rename('series') tm.assert_series_equal(result.reindex_like(expected), expected) - def test_agg_apply_evaluate_lambdas_the_same(self): + def test_agg_apply_evaluate_lambdas_the_same(self, string_series): # test that we are evaluating row-by-row first # before vectorized evaluation - result = self.series.apply(lambda x: str(x)) - expected = self.series.agg(lambda x: str(x)) + result = string_series.apply(lambda x: str(x)) + expected = string_series.agg(lambda x: str(x)) tm.assert_series_equal(result, expected) - result = self.series.apply(str) - expected = self.series.agg(str) + result = string_series.apply(str) + expected = string_series.agg(str) tm.assert_series_equal(result, expected) - def test_with_nested_series(self): + def test_with_nested_series(self, datetime_series): # GH 2316 # .agg with a reducer and a transform, what to do - result = self.ts.apply(lambda x: Series( + result = datetime_series.apply(lambda x: Series( [x, x ** 2], index=['x', 'x^2'])) - expected = DataFrame({'x': self.ts, 'x^2': self.ts ** 2}) + expected = DataFrame({'x': datetime_series, + 'x^2': datetime_series ** 2}) tm.assert_frame_equal(result, expected) - result = self.ts.agg(lambda x: Series( + 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): + def test_replicate_describe(self, string_series): # this also tests a result set that is all scalars - expected = self.series.describe() - result = self.series.apply(OrderedDict( + expected = string_series.describe() + result = string_series.apply(OrderedDict( [('count', 'count'), ('mean', 'mean'), ('std', 'std'), @@ -309,13 +310,13 @@ def test_replicate_describe(self): ('max', 'max')])) assert_series_equal(result, expected) - def test_reduce(self): + def test_reduce(self, string_series): # reductions with named functions - result = self.series.agg(['sum', 'mean']) - expected = Series([self.series.sum(), - self.series.mean()], + result = string_series.agg(['sum', 'mean']) + expected = Series([string_series.sum(), + string_series.mean()], ['sum', 'mean'], - name=self.series.name) + name=string_series.name) assert_series_equal(result, expected) def test_non_callable_aggregates(self): @@ -414,9 +415,9 @@ def test_agg_cython_table_raises(self, series, func, expected): series.agg(func) -class TestSeriesMap(TestData): +class TestSeriesMap(): - def test_map(self): + def test_map(self, datetime_series): index, data = tm.getMixedTypeDict() source = Series(data['B'], index=data['C']) @@ -434,8 +435,8 @@ def test_map(self): assert v == source[target[k]] # function - result = self.ts.map(lambda x: x * 2) - tm.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]) @@ -500,10 +501,10 @@ def test_map_type_inference(self): s2 = s.map(lambda x: np.where(x == 0, 0, 1)) 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))) + result = string_series.map(lambda x: Decimal(str(x))) assert result.dtype == np.object_ assert isinstance(result[0], Decimal) From 4cc0a71c729aaf9a5cf46a2c4b3fd5e0a4fd37a9 Mon Sep 17 00:00:00 2001 From: Thierry Moisan Date: Sun, 23 Sep 2018 11:32:57 -0400 Subject: [PATCH 47/87] DOC: Fix DataFrame.to_csv docstring and add an example. GH22459 (#22475) --- pandas/core/generic.py | 116 ++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index b72d8cbf02bc6..9d19b02c4d1fb 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9501,80 +9501,100 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, quotechar='"', line_terminator='\n', chunksize=None, tupleize_cols=None, date_format=None, doublequote=True, escapechar=None, decimal='.'): - r"""Write object to a comma-separated values (csv) file + r""" + Write object to a comma-separated values (csv) file. + + .. versionchanged:: 0.24.0 + The order of arguments for Series was changed. Parameters ---------- - path_or_buf : string or file handle, default None + path_or_buf : str or file handle, default None File path or object, if None is provided the result is returned as a string. .. versionchanged:: 0.24.0 Was previously named "path" for Series. - sep : character, default ',' - Field delimiter for the output file. - na_rep : string, default '' - Missing data representation - float_format : string, default None - Format string for floating point numbers + sep : str, default ',' + String of length 1. Field delimiter for the output file. + na_rep : str, default '' + Missing data representation. + float_format : str, default None + Format string for floating point numbers. columns : sequence, optional - Columns to write - header : boolean or list of string, default True + Columns to write. + header : bool or list of str, default True Write out the column names. If a list of strings is given it is - assumed to be aliases for the column names + assumed to be aliases for the column names. .. versionchanged:: 0.24.0 Previously defaulted to False for Series. - index : boolean, default True - Write row names (index) - index_label : string or sequence, or False, default None + index : bool, default True + Write row names (index). + index_label : str or sequence, or False, default None Column label for index column(s) if desired. If None is given, and `header` and `index` are True, then the index names are used. A - sequence should be given if the object uses MultiIndex. If + sequence should be given if the object uses MultiIndex. If False do not print fields for index names. Use index_label=False - for easier importing in R + for easier importing in R. mode : str - Python write mode, default 'w' - encoding : string, optional + Python write mode, default 'w'. + encoding : str, optional A string representing the encoding to use in the output file, defaults to 'ascii' on Python 2 and 'utf-8' on Python 3. - compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None}, - default 'infer' - If 'infer' and `path_or_buf` is path-like, then detect compression - from the following extensions: '.gz', '.bz2', '.zip' or '.xz' - (otherwise no compression). - + compression : str, default 'infer' + Compression mode among the following possible values: {'infer', + 'gzip', 'bz2', 'zip', 'xz', None}. If 'infer' and `path_or_buf` + is path-like, then detect compression from the following + extensions: '.gz', '.bz2', '.zip' or '.xz'. (otherwise no + compression). .. versionchanged:: 0.24.0 - 'infer' option added and set to default - line_terminator : string, default ``'\n'`` - The newline character or character sequence to use in the output - file + 'infer' option added and set to default. quoting : optional constant from csv module - defaults to csv.QUOTE_MINIMAL. If you have set a `float_format` + Defaults to csv.QUOTE_MINIMAL. If you have set a `float_format` then floats are converted to strings and thus csv.QUOTE_NONNUMERIC - will treat them as non-numeric - quotechar : string (length 1), default '\"' - character used to quote fields - doublequote : boolean, default True - Control quoting of `quotechar` inside a field - escapechar : string (length 1), default None - character used to escape `sep` and `quotechar` when appropriate + will treat them as non-numeric. + quotechar : str, default '\"' + String of length 1. Character used to quote fields. + line_terminator : string, default ``'\n'`` + The newline character or character sequence to use in the output + file. chunksize : int or None - rows to write at a time - tupleize_cols : boolean, default False - .. deprecated:: 0.21.0 - This argument will be removed and will always write each row - of the multi-index as a separate row in the CSV file. - + Rows to write at a time. + tupleize_cols : bool, default False Write MultiIndex columns as a list of tuples (if True) or in the new, expanded format, where each MultiIndex column is a row in the CSV (if False). - date_format : string, default None - Format string for datetime objects - decimal: string, default '.' + .. deprecated:: 0.21.0 + This argument will be removed and will always write each row + of the multi-index as a separate row in the CSV file. + date_format : str, default None + Format string for datetime objects. + doublequote : bool, default True + Control quoting of `quotechar` inside a field. + escapechar : str, default None + String of length 1. Character used to escape `sep` and `quotechar` + when appropriate. + decimal : str, default '.' Character recognized as decimal separator. E.g. use ',' for - European data + European data. - .. versionchanged:: 0.24.0 - The order of arguments for Series was changed. + Returns + ------- + None or str + If path_or_buf is None, returns the resulting csv format as a + string. Otherwise returns None. + + See Also + -------- + pandas.read_csv : Load a CSV file into a DataFrame. + pandas.to_excel: Load an Excel file into a DataFrame. + + Examples + -------- + >>> df = pd.DataFrame({'name': ['Raphael', 'Donatello'], + ... 'mask': ['red', 'purple'], + ... 'weapon': ['sai', 'bo staff']}) + >>> df.to_csv(index=False) + 'name,mask,weapon\nRaphael,red,sai\nDonatello,purple,bo staff\n' """ df = self if isinstance(self, ABCDataFrame) else self.to_frame() From 44a9b167bda9654ce60588cf2dcee88e4bad831d Mon Sep 17 00:00:00 2001 From: h-vetinari <33685575+h-vetinari@users.noreply.github.com> Date: Sun, 23 Sep 2018 20:28:40 +0200 Subject: [PATCH 48/87] TST/CLN: Fixturize tests/frame/test_apply (#22735) --- pandas/tests/frame/test_apply.py | 362 +++++++++++++++---------------- 1 file changed, 172 insertions(+), 190 deletions(-) diff --git a/pandas/tests/frame/test_apply.py b/pandas/tests/frame/test_apply.py index 7b71240a34b5c..e27115cfc255b 100644 --- a/pandas/tests/frame/test_apply.py +++ b/pandas/tests/frame/test_apply.py @@ -23,25 +23,36 @@ assert_frame_equal) import pandas.util.testing as tm from pandas.conftest import _get_cython_table_params -from pandas.tests.frame.common import TestData -class TestDataFrameApply(TestData): +@pytest.fixture +def int_frame_const_col(): + """ + Fixture for DataFrame of ints which are constant per column + + Columns are ['A', 'B', 'C'], with values (per column): [1, 2, 3] + """ + df = DataFrame(np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, + columns=['A', 'B', 'C']) + return df + + +class TestDataFrameApply(): - def test_apply(self): + def test_apply(self, float_frame): with np.errstate(all='ignore'): # ufunc - applied = self.frame.apply(np.sqrt) - tm.assert_series_equal(np.sqrt(self.frame['A']), applied['A']) + applied = float_frame.apply(np.sqrt) + tm.assert_series_equal(np.sqrt(float_frame['A']), applied['A']) # aggregator - applied = self.frame.apply(np.mean) - assert applied['A'] == np.mean(self.frame['A']) + applied = float_frame.apply(np.mean) + assert applied['A'] == np.mean(float_frame['A']) - d = self.frame.index[0] - applied = self.frame.apply(np.mean, axis=1) - assert applied[d] == np.mean(self.frame.xs(d)) - assert applied.index is self.frame.index # want this + d = float_frame.index[0] + applied = float_frame.apply(np.mean, axis=1) + assert applied[d] == np.mean(float_frame.xs(d)) + assert applied.index is float_frame.index # want this # invalid axis df = DataFrame( @@ -65,22 +76,22 @@ def test_apply_mixed_datetimelike(self): result = df.apply(lambda x: x, axis=1) assert_frame_equal(result, df) - def test_apply_empty(self): + def test_apply_empty(self, float_frame, empty_frame): # empty - applied = self.empty.apply(np.sqrt) + applied = empty_frame.apply(np.sqrt) assert applied.empty - applied = self.empty.apply(np.mean) + applied = empty_frame.apply(np.mean) assert applied.empty - no_rows = self.frame[:0] + no_rows = float_frame[:0] result = no_rows.apply(lambda x: x.mean()) - expected = Series(np.nan, index=self.frame.columns) + expected = Series(np.nan, index=float_frame.columns) assert_series_equal(result, expected) - no_cols = self.frame.loc[:, []] + no_cols = float_frame.loc[:, []] result = no_cols.apply(lambda x: x.mean(), axis=1) - expected = Series(np.nan, index=self.frame.index) + expected = Series(np.nan, index=float_frame.index) assert_series_equal(result, expected) # 2476 @@ -88,12 +99,12 @@ def test_apply_empty(self): rs = xp.apply(lambda x: x['a'], axis=1) assert_frame_equal(xp, rs) - def test_apply_with_reduce_empty(self): + def test_apply_with_reduce_empty(self, empty_frame): # reduce with an empty DataFrame x = [] - result = self.empty.apply(x.append, axis=1, result_type='expand') - assert_frame_equal(result, self.empty) - result = self.empty.apply(x.append, axis=1, result_type='reduce') + result = empty_frame.apply(x.append, axis=1, result_type='expand') + assert_frame_equal(result, empty_frame) + result = empty_frame.apply(x.append, axis=1, result_type='reduce') assert_series_equal(result, Series( [], index=pd.Index([], dtype=object))) @@ -107,10 +118,10 @@ def test_apply_with_reduce_empty(self): # Ensure that x.append hasn't been called assert x == [] - def test_apply_deprecate_reduce(self): + def test_apply_deprecate_reduce(self, empty_frame): x = [] with tm.assert_produces_warning(FutureWarning): - self.empty.apply(x.append, axis=1, reduce=True) + empty_frame.apply(x.append, axis=1, reduce=True) def test_apply_standard_nonunique(self): df = DataFrame( @@ -130,110 +141,98 @@ def test_apply_standard_nonunique(self): pytest.param([], {'numeric_only': True}, id='optional_kwds'), pytest.param([1, None], {'numeric_only': True}, id='args_and_kwds') ]) - def test_apply_with_string_funcs(self, func, args, kwds): - result = self.frame.apply(func, *args, **kwds) - expected = getattr(self.frame, func)(*args, **kwds) + def test_apply_with_string_funcs(self, float_frame, func, args, kwds): + result = float_frame.apply(func, *args, **kwds) + expected = getattr(float_frame, func)(*args, **kwds) tm.assert_series_equal(result, expected) - def test_apply_broadcast_deprecated(self): + def test_apply_broadcast_deprecated(self, float_frame): with tm.assert_produces_warning(FutureWarning): - self.frame.apply(np.mean, broadcast=True) + float_frame.apply(np.mean, broadcast=True) - def test_apply_broadcast(self): + def test_apply_broadcast(self, float_frame, int_frame_const_col): # scalars - result = self.frame.apply(np.mean, result_type='broadcast') - expected = DataFrame([self.frame.mean()], index=self.frame.index) + result = float_frame.apply(np.mean, result_type='broadcast') + expected = DataFrame([float_frame.mean()], index=float_frame.index) tm.assert_frame_equal(result, expected) - result = self.frame.apply(np.mean, axis=1, result_type='broadcast') - m = self.frame.mean(axis=1) - expected = DataFrame({c: m for c in self.frame.columns}) + result = float_frame.apply(np.mean, axis=1, result_type='broadcast') + m = float_frame.mean(axis=1) + expected = DataFrame({c: m for c in float_frame.columns}) tm.assert_frame_equal(result, expected) # lists - result = self.frame.apply( - lambda x: list(range(len(self.frame.columns))), + result = float_frame.apply( + lambda x: list(range(len(float_frame.columns))), axis=1, result_type='broadcast') - m = list(range(len(self.frame.columns))) - expected = DataFrame([m] * len(self.frame.index), + m = list(range(len(float_frame.columns))) + expected = DataFrame([m] * len(float_frame.index), dtype='float64', - index=self.frame.index, - columns=self.frame.columns) + index=float_frame.index, + columns=float_frame.columns) tm.assert_frame_equal(result, expected) - result = self.frame.apply(lambda x: list(range(len(self.frame.index))), - result_type='broadcast') - m = list(range(len(self.frame.index))) - expected = DataFrame({c: m for c in self.frame.columns}, + result = float_frame.apply(lambda x: + list(range(len(float_frame.index))), + result_type='broadcast') + m = list(range(len(float_frame.index))) + expected = DataFrame({c: m for c in float_frame.columns}, dtype='float64', - index=self.frame.index) + index=float_frame.index) tm.assert_frame_equal(result, expected) # preserve columns - df = DataFrame(np.tile(np.arange(3), 6).reshape(6, -1) + 1, - columns=list('ABC')) - result = df.apply(lambda x: [1, 2, 3], - axis=1, - result_type='broadcast') + df = int_frame_const_col + result = df.apply(lambda x: [1, 2, 3], axis=1, result_type='broadcast') tm.assert_frame_equal(result, df) - df = DataFrame(np.tile(np.arange(3), 6).reshape(6, -1) + 1, - columns=list('ABC')) + df = int_frame_const_col result = df.apply(lambda x: Series([1, 2, 3], index=list('abc')), - axis=1, - result_type='broadcast') + axis=1, result_type='broadcast') expected = df.copy() tm.assert_frame_equal(result, expected) - def test_apply_broadcast_error(self): - df = DataFrame( - np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['A', 'B', 'C']) + def test_apply_broadcast_error(self, int_frame_const_col): + df = int_frame_const_col # > 1 ndim with pytest.raises(ValueError): df.apply(lambda x: np.array([1, 2]).reshape(-1, 2), - axis=1, - result_type='broadcast') + axis=1, result_type='broadcast') # cannot broadcast with pytest.raises(ValueError): - df.apply(lambda x: [1, 2], - axis=1, - result_type='broadcast') + df.apply(lambda x: [1, 2], axis=1, result_type='broadcast') with pytest.raises(ValueError): - df.apply(lambda x: Series([1, 2]), - axis=1, - result_type='broadcast') + df.apply(lambda x: Series([1, 2]), axis=1, result_type='broadcast') - def test_apply_raw(self): - result0 = self.frame.apply(np.mean, raw=True) - result1 = self.frame.apply(np.mean, axis=1, raw=True) + def test_apply_raw(self, float_frame): + result0 = float_frame.apply(np.mean, raw=True) + result1 = float_frame.apply(np.mean, axis=1, raw=True) - expected0 = self.frame.apply(lambda x: x.values.mean()) - expected1 = self.frame.apply(lambda x: x.values.mean(), axis=1) + expected0 = float_frame.apply(lambda x: x.values.mean()) + expected1 = float_frame.apply(lambda x: x.values.mean(), axis=1) assert_series_equal(result0, expected0) assert_series_equal(result1, expected1) # no reduction - result = self.frame.apply(lambda x: x * 2, raw=True) - expected = self.frame * 2 + result = float_frame.apply(lambda x: x * 2, raw=True) + expected = float_frame * 2 assert_frame_equal(result, expected) - def test_apply_axis1(self): - d = self.frame.index[0] - tapplied = self.frame.apply(np.mean, axis=1) - assert tapplied[d] == np.mean(self.frame.xs(d)) + def test_apply_axis1(self, float_frame): + d = float_frame.index[0] + tapplied = float_frame.apply(np.mean, axis=1) + assert tapplied[d] == np.mean(float_frame.xs(d)) - def test_apply_ignore_failures(self): - result = frame_apply(self.mixed_frame, - np.mean, 0, + def test_apply_ignore_failures(self, float_string_frame): + result = frame_apply(float_string_frame, np.mean, 0, ignore_failures=True).apply_standard() - expected = self.mixed_frame._get_numeric_data().apply(np.mean) + expected = float_string_frame._get_numeric_data().apply(np.mean) assert_series_equal(result, expected) def test_apply_mixed_dtype_corner(self): @@ -288,7 +287,7 @@ def _checkit(axis=0, raw=False): result = no_cols.apply(lambda x: x.mean(), result_type='broadcast') assert isinstance(result, DataFrame) - def test_apply_with_args_kwds(self): + def test_apply_with_args_kwds(self, float_frame): def add_some(x, howmuch=0): return x + howmuch @@ -298,26 +297,26 @@ def agg_and_add(x, howmuch=0): def subtract_and_divide(x, sub, divide=1): return (x - sub) / divide - result = self.frame.apply(add_some, howmuch=2) - exp = self.frame.apply(lambda x: x + 2) + result = float_frame.apply(add_some, howmuch=2) + exp = float_frame.apply(lambda x: x + 2) assert_frame_equal(result, exp) - result = self.frame.apply(agg_and_add, howmuch=2) - exp = self.frame.apply(lambda x: x.mean() + 2) + result = float_frame.apply(agg_and_add, howmuch=2) + exp = float_frame.apply(lambda x: x.mean() + 2) assert_series_equal(result, exp) - res = self.frame.apply(subtract_and_divide, args=(2,), divide=2) - exp = self.frame.apply(lambda x: (x - 2.) / 2.) + res = float_frame.apply(subtract_and_divide, args=(2,), divide=2) + exp = float_frame.apply(lambda x: (x - 2.) / 2.) assert_frame_equal(res, exp) - def test_apply_yield_list(self): - result = self.frame.apply(list) - assert_frame_equal(result, self.frame) + def test_apply_yield_list(self, float_frame): + result = float_frame.apply(list) + assert_frame_equal(result, float_frame) - def test_apply_reduce_Series(self): - self.frame.loc[::2, 'A'] = np.nan - expected = self.frame.mean(1) - result = self.frame.apply(np.mean, axis=1) + def test_apply_reduce_Series(self, float_frame): + float_frame.loc[::2, 'A'] = np.nan + expected = float_frame.mean(1) + result = float_frame.apply(np.mean, axis=1) assert_series_equal(result, expected) def test_apply_differently_indexed(self): @@ -408,31 +407,31 @@ def test_apply_convert_objects(self): result = data.apply(lambda x: x, axis=1) assert_frame_equal(result._convert(datetime=True), data) - def test_apply_attach_name(self): - result = self.frame.apply(lambda x: x.name) - expected = Series(self.frame.columns, index=self.frame.columns) + def test_apply_attach_name(self, float_frame): + result = float_frame.apply(lambda x: x.name) + expected = Series(float_frame.columns, index=float_frame.columns) assert_series_equal(result, expected) - result = self.frame.apply(lambda x: x.name, axis=1) - expected = Series(self.frame.index, index=self.frame.index) + result = float_frame.apply(lambda x: x.name, axis=1) + expected = Series(float_frame.index, index=float_frame.index) assert_series_equal(result, expected) # non-reductions - result = self.frame.apply(lambda x: np.repeat(x.name, len(x))) - expected = DataFrame(np.tile(self.frame.columns, - (len(self.frame.index), 1)), - index=self.frame.index, - columns=self.frame.columns) + result = float_frame.apply(lambda x: np.repeat(x.name, len(x))) + expected = DataFrame(np.tile(float_frame.columns, + (len(float_frame.index), 1)), + index=float_frame.index, + columns=float_frame.columns) assert_frame_equal(result, expected) - result = self.frame.apply(lambda x: np.repeat(x.name, len(x)), - axis=1) - expected = Series(np.repeat(t[0], len(self.frame.columns)) - for t in self.frame.itertuples()) - expected.index = self.frame.index + result = float_frame.apply(lambda x: np.repeat(x.name, len(x)), + axis=1) + expected = Series(np.repeat(t[0], len(float_frame.columns)) + for t in float_frame.itertuples()) + expected.index = float_frame.index assert_series_equal(result, expected) - def test_apply_multi_index(self): + def test_apply_multi_index(self, float_frame): index = MultiIndex.from_arrays([['a', 'a', 'b'], ['c', 'd', 'd']]) s = DataFrame([[1, 2], [3, 4], [5, 6]], index=index, @@ -463,13 +462,13 @@ def test_apply_dict(self): assert_frame_equal(reduce_false, df) assert_series_equal(reduce_none, dicts) - def test_applymap(self): - applied = self.frame.applymap(lambda x: x * 2) - tm.assert_frame_equal(applied, self.frame * 2) - self.frame.applymap(type) + def test_applymap(self, float_frame): + applied = float_frame.applymap(lambda x: x * 2) + tm.assert_frame_equal(applied, float_frame * 2) + float_frame.applymap(type) # gh-465: function returning tuples - result = self.frame.applymap(lambda x: (x, x)) + result = float_frame.applymap(lambda x: (x, x)) assert isinstance(result['A'][0], tuple) # gh-2909: object conversion to float in constructor? @@ -721,33 +720,27 @@ def test_consistent_coerce_for_shapes(self): expected = Series([[1, 2] for t in df.itertuples()]) assert_series_equal(result, expected) - def test_consistent_names(self): + def test_consistent_names(self, int_frame_const_col): # if a Series is returned, we should use the resulting index names - df = DataFrame( - np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['A', 'B', 'C']) + df = int_frame_const_col result = df.apply(lambda x: Series([1, 2, 3], index=['test', 'other', 'cols']), axis=1) - expected = DataFrame( - np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['test', 'other', 'cols']) + expected = int_frame_const_col.rename(columns={'A': 'test', + 'B': 'other', + 'C': 'cols'}) assert_frame_equal(result, expected) - result = df.apply( - lambda x: pd.Series([1, 2], index=['test', 'other']), axis=1) - expected = DataFrame( - np.tile(np.arange(2, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['test', 'other']) + result = df.apply(lambda x: Series([1, 2], index=['test', 'other']), + axis=1) + expected = expected[['test', 'other']] assert_frame_equal(result, expected) - def test_result_type(self): + def test_result_type(self, int_frame_const_col): # result_type should be consistent no matter which # path we take in the code - df = DataFrame( - np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['A', 'B', 'C']) + df = int_frame_const_col result = df.apply(lambda x: [1, 2, 3], axis=1, result_type='expand') expected = df.copy() @@ -765,11 +758,8 @@ def test_result_type(self): assert_frame_equal(result, expected) columns = ['other', 'col', 'names'] - result = df.apply( - lambda x: pd.Series([1, 2, 3], - index=columns), - axis=1, - result_type='broadcast') + result = df.apply(lambda x: Series([1, 2, 3], index=columns), + axis=1, result_type='broadcast') expected = df.copy() assert_frame_equal(result, expected) @@ -780,24 +770,18 @@ def test_result_type(self): # series result with other index columns = ['other', 'col', 'names'] - result = df.apply( - lambda x: pd.Series([1, 2, 3], index=columns), - axis=1) + result = df.apply(lambda x: Series([1, 2, 3], index=columns), axis=1) expected = df.copy() expected.columns = columns assert_frame_equal(result, expected) @pytest.mark.parametrize("result_type", ['foo', 1]) - def test_result_type_error(self, result_type): + def test_result_type_error(self, result_type, int_frame_const_col): # allowed result_type - df = DataFrame( - np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['A', 'B', 'C']) + df = int_frame_const_col with pytest.raises(ValueError): - df.apply(lambda x: [1, 2, 3], - axis=1, - result_type=result_type) + df.apply(lambda x: [1, 2, 3], axis=1, result_type=result_type) @pytest.mark.parametrize( "box", @@ -805,19 +789,17 @@ def test_result_type_error(self, result_type): lambda x: tuple(x), lambda x: np.array(x, dtype='int64')], ids=['list', 'tuple', 'array']) - def test_consistency_for_boxed(self, box): + def test_consistency_for_boxed(self, box, int_frame_const_col): # passing an array or list should not affect the output shape - df = DataFrame( - np.tile(np.arange(3, dtype='int64'), 6).reshape(6, -1) + 1, - columns=['A', 'B', 'C']) + df = int_frame_const_col result = df.apply(lambda x: box([1, 2]), axis=1) expected = Series([box([1, 2]) for t in df.itertuples()]) assert_series_equal(result, expected) result = df.apply(lambda x: box([1, 2]), axis=1, result_type='expand') - expected = DataFrame( - np.tile(np.arange(2, dtype='int64'), 6).reshape(6, -1) + 1) + expected = int_frame_const_col[['A', 'B']].rename(columns={'A': 0, + 'B': 1}) assert_frame_equal(result, expected) @@ -840,71 +822,71 @@ def zip_frames(frames, axis=1): return pd.DataFrame(zipped) -class TestDataFrameAggregate(TestData): +class TestDataFrameAggregate(): - def test_agg_transform(self, axis): + def test_agg_transform(self, axis, float_frame): other_axis = 1 if axis in {0, 'index'} else 0 with np.errstate(all='ignore'): - f_abs = np.abs(self.frame) - f_sqrt = np.sqrt(self.frame) + f_abs = np.abs(float_frame) + f_sqrt = np.sqrt(float_frame) # ufunc - result = self.frame.transform(np.sqrt, axis=axis) + result = float_frame.transform(np.sqrt, axis=axis) expected = f_sqrt.copy() assert_frame_equal(result, expected) - result = self.frame.apply(np.sqrt, axis=axis) + result = float_frame.apply(np.sqrt, axis=axis) assert_frame_equal(result, expected) - result = self.frame.transform(np.sqrt, axis=axis) + result = float_frame.transform(np.sqrt, axis=axis) assert_frame_equal(result, expected) # list-like - result = self.frame.apply([np.sqrt], axis=axis) + result = float_frame.apply([np.sqrt], axis=axis) expected = f_sqrt.copy() if axis in {0, 'index'}: expected.columns = pd.MultiIndex.from_product( - [self.frame.columns, ['sqrt']]) + [float_frame.columns, ['sqrt']]) else: expected.index = pd.MultiIndex.from_product( - [self.frame.index, ['sqrt']]) + [float_frame.index, ['sqrt']]) assert_frame_equal(result, expected) - result = self.frame.transform([np.sqrt], axis=axis) + result = float_frame.transform([np.sqrt], axis=axis) 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 - result = self.frame.apply([np.abs, np.sqrt], axis=axis) + result = float_frame.apply([np.abs, np.sqrt], axis=axis) expected = zip_frames([f_abs, f_sqrt], axis=other_axis) if axis in {0, 'index'}: expected.columns = pd.MultiIndex.from_product( - [self.frame.columns, ['absolute', 'sqrt']]) + [float_frame.columns, ['absolute', 'sqrt']]) else: expected.index = pd.MultiIndex.from_product( - [self.frame.index, ['absolute', 'sqrt']]) + [float_frame.index, ['absolute', 'sqrt']]) assert_frame_equal(result, expected) - result = self.frame.transform([np.abs, 'sqrt'], axis=axis) + result = float_frame.transform([np.abs, 'sqrt'], axis=axis) assert_frame_equal(result, expected) - def test_transform_and_agg_err(self, axis): + def test_transform_and_agg_err(self, axis, float_frame): # cannot both transform and agg def f(): - self.frame.transform(['max', 'min'], axis=axis) + float_frame.transform(['max', 'min'], axis=axis) pytest.raises(ValueError, f) def f(): with np.errstate(all='ignore'): - self.frame.agg(['max', 'sqrt'], axis=axis) + float_frame.agg(['max', 'sqrt'], axis=axis) pytest.raises(ValueError, f) def f(): with np.errstate(all='ignore'): - self.frame.transform(['max', 'sqrt'], axis=axis) + float_frame.transform(['max', 'sqrt'], axis=axis) pytest.raises(ValueError, f) df = pd.DataFrame({'A': range(5), 'B': 5}) @@ -974,49 +956,49 @@ def test_agg_dict_nested_renaming_depr(self): df.agg({'A': {'foo': 'min'}, 'B': {'bar': 'max'}}) - def test_agg_reduce(self, axis): + def test_agg_reduce(self, axis, float_frame): other_axis = 1 if axis in {0, 'index'} else 0 - name1, name2 = self.frame.axes[other_axis].unique()[:2].sort_values() + name1, name2 = float_frame.axes[other_axis].unique()[:2].sort_values() # all reducers - expected = pd.concat([self.frame.mean(axis=axis), - self.frame.max(axis=axis), - self.frame.sum(axis=axis), + expected = pd.concat([float_frame.mean(axis=axis), + float_frame.max(axis=axis), + float_frame.sum(axis=axis), ], axis=1) expected.columns = ['mean', 'max', 'sum'] expected = expected.T if axis in {0, 'index'} else expected - result = self.frame.agg(['mean', 'max', 'sum'], axis=axis) + result = float_frame.agg(['mean', 'max', 'sum'], axis=axis) assert_frame_equal(result, expected) # dict input with scalars func = OrderedDict([(name1, 'mean'), (name2, 'sum')]) - result = self.frame.agg(func, axis=axis) - expected = Series([self.frame.loc(other_axis)[name1].mean(), - self.frame.loc(other_axis)[name2].sum()], + result = float_frame.agg(func, axis=axis) + expected = Series([float_frame.loc(other_axis)[name1].mean(), + float_frame.loc(other_axis)[name2].sum()], index=[name1, name2]) assert_series_equal(result, expected) # dict input with lists func = OrderedDict([(name1, ['mean']), (name2, ['sum'])]) - result = self.frame.agg(func, axis=axis) + result = float_frame.agg(func, axis=axis) expected = DataFrame({ - name1: Series([self.frame.loc(other_axis)[name1].mean()], + name1: Series([float_frame.loc(other_axis)[name1].mean()], index=['mean']), - name2: Series([self.frame.loc(other_axis)[name2].sum()], + name2: Series([float_frame.loc(other_axis)[name2].sum()], index=['sum'])}) expected = expected.T if axis in {1, 'columns'} else expected assert_frame_equal(result, expected) # dict input with lists with multiple func = OrderedDict([(name1, ['mean', 'sum']), (name2, ['sum', 'max'])]) - result = self.frame.agg(func, axis=axis) + result = float_frame.agg(func, axis=axis) expected = DataFrame(OrderedDict([ - (name1, Series([self.frame.loc(other_axis)[name1].mean(), - self.frame.loc(other_axis)[name1].sum()], + (name1, Series([float_frame.loc(other_axis)[name1].mean(), + float_frame.loc(other_axis)[name1].sum()], index=['mean', 'sum'])), - (name2, Series([self.frame.loc(other_axis)[name2].sum(), - self.frame.loc(other_axis)[name2].max()], + (name2, Series([float_frame.loc(other_axis)[name2].sum(), + float_frame.loc(other_axis)[name2].max()], index=['sum', 'max'])), ])) expected = expected.T if axis in {1, 'columns'} else expected From 2ab57b2dfc7005e580698c3e0bdf9478e96647db Mon Sep 17 00:00:00 2001 From: Scott McAllister Date: Sun, 23 Sep 2018 16:11:02 -0400 Subject: [PATCH 49/87] BUG:reorder type check/conversion so wide_to_long handles str arg for stubnames. GH22468 (#22490) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/reshape/melt.py | 6 +++--- pandas/tests/reshape/test_melt.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 618d7454c67fe..9d559acfa59e7 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -802,6 +802,7 @@ Reshaping - Bug in :meth:`DataFrame.replace` raises ``RecursionError`` when replacing empty lists (:issue:`22083`) - Bug in :meth:`Series.replace` and meth:`DataFrame.replace` when dict is used as the ``to_replace`` value and one key in the dict is is another key's value, the results were inconsistent between using integer key and using string key (:issue:`20656`) - Bug in :meth:`DataFrame.drop_duplicates` for empty ``DataFrame`` which incorrectly raises an error (:issue:`20516`) +- Bug in :func:`pandas.wide_to_long` when a string is passed to the stubnames argument and a column name is a substring of that stubname (:issue:`22468`) Build Changes ^^^^^^^^^^^^^ diff --git a/pandas/core/reshape/melt.py b/pandas/core/reshape/melt.py index f4b96c8f1ca49..26221143c0cdf 100644 --- a/pandas/core/reshape/melt.py +++ b/pandas/core/reshape/melt.py @@ -409,14 +409,14 @@ def melt_stub(df, stub, i, j, value_vars, sep): return newdf.set_index(i + [j]) - if any(col in stubnames for col in df.columns): - raise ValueError("stubname can't be identical to a column name") - if not is_list_like(stubnames): stubnames = [stubnames] else: stubnames = list(stubnames) + if any(col in stubnames for col in df.columns): + raise ValueError("stubname can't be identical to a column name") + if not is_list_like(i): i = [i] else: diff --git a/pandas/tests/reshape/test_melt.py b/pandas/tests/reshape/test_melt.py index 81570de7586de..e83a2cb483de7 100644 --- a/pandas/tests/reshape/test_melt.py +++ b/pandas/tests/reshape/test_melt.py @@ -640,3 +640,24 @@ def test_float_suffix(self): 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) From 64b88e86677349de66071dffd74b5c3254283e2a Mon Sep 17 00:00:00 2001 From: Troels Nielsen Date: Mon, 24 Sep 2018 05:53:09 +0200 Subject: [PATCH 50/87] BUG: read_table and read_csv crash (#22750) A missing null-pointer check made read_table and read_csv prone to crash on badly encoded text. Add null-pointer check. Closes gh-22748. --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/_libs/src/parser/io.c | 6 +++++- pandas/tests/io/parser/common.py | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 9d559acfa59e7..6c91b6374b8af 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -756,6 +756,7 @@ I/O - :func:`read_html()` no longer ignores all-whitespace ```` within ```` when considering the ``skiprows`` and ``header`` arguments. Previously, users had to decrease their ``header`` and ``skiprows`` values on such tables to work around the issue. (:issue:`21641`) - :func:`read_excel()` will correctly show the deprecation warning for previously deprecated ``sheetname`` (:issue:`17994`) +- :func:`read_csv()` and func:`read_table()` will throw ``UnicodeError`` and not coredump on badly encoded strings (:issue:`22748`) - :func:`read_csv()` will correctly parse timezone-aware datetimes (:issue:`22256`) - :func:`read_sas()` will parse numbers in sas7bdat-files that have width less than 8 bytes correctly. (:issue:`21616`) - :func:`read_sas()` will correctly parse sas7bdat files with many columns (:issue:`22628`) diff --git a/pandas/_libs/src/parser/io.c b/pandas/_libs/src/parser/io.c index 8300e889d4157..19271c78501ba 100644 --- a/pandas/_libs/src/parser/io.c +++ b/pandas/_libs/src/parser/io.c @@ -150,7 +150,11 @@ void *buffer_rd_bytes(void *source, size_t nbytes, size_t *bytes_read, return NULL; } else if (!PyBytes_Check(result)) { tmp = PyUnicode_AsUTF8String(result); - Py_XDECREF(result); + Py_DECREF(result); + if (tmp == NULL) { + PyGILState_Release(state); + return NULL; + } result = tmp; } diff --git a/pandas/tests/io/parser/common.py b/pandas/tests/io/parser/common.py index 9e871d27f0ce8..064385e60c4ec 100644 --- a/pandas/tests/io/parser/common.py +++ b/pandas/tests/io/parser/common.py @@ -9,6 +9,7 @@ import sys from datetime import datetime from collections import OrderedDict +from io import TextIOWrapper import pytest import numpy as np @@ -1609,3 +1610,11 @@ def test_skip_bad_lines(self): val = sys.stderr.getvalue() assert 'Skipping line 3' in val assert 'Skipping line 5' in val + + def test_buffer_rd_bytes_bad_unicode(self): + # Regression test for #22748 + t = BytesIO(b"\xB0") + if PY3: + t = TextIOWrapper(t, encoding='ascii', errors='surrogateescape') + with pytest.raises(UnicodeError): + pd.read_csv(t, encoding='UTF-8') From 3ab9dbd64b4a057eda53b841f43999fb319869e9 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Mon, 24 Sep 2018 14:32:07 +0200 Subject: [PATCH 51/87] DOC: fixup spacing in to_csv docstring (GH22475) (#22816) --- pandas/core/generic.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 9d19b02c4d1fb..19ac4b49358d4 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9512,6 +9512,7 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, path_or_buf : str or file handle, default None File path or object, if None is provided the result is returned as a string. + .. versionchanged:: 0.24.0 Was previously named "path" for Series. sep : str, default ',' @@ -9525,6 +9526,7 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, header : bool or list of str, default True Write out the column names. If a list of strings is given it is assumed to be aliases for the column names. + .. versionchanged:: 0.24.0 Previously defaulted to False for Series. index : bool, default True @@ -9546,6 +9548,7 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, is path-like, then detect compression from the following extensions: '.gz', '.bz2', '.zip' or '.xz'. (otherwise no compression). + .. versionchanged:: 0.24.0 'infer' option added and set to default. quoting : optional constant from csv module @@ -9563,6 +9566,7 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, Write MultiIndex columns as a list of tuples (if True) or in the new, expanded format, where each MultiIndex column is a row in the CSV (if False). + .. deprecated:: 0.21.0 This argument will be removed and will always write each row of the multi-index as a separate row in the CSV file. @@ -9586,7 +9590,7 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, See Also -------- pandas.read_csv : Load a CSV file into a DataFrame. - pandas.to_excel: Load an Excel file into a DataFrame. + pandas.to_excel : Load an Excel file into a DataFrame. Examples -------- From 183a41620c596bfc58edef5deb53d53e8bb4b798 Mon Sep 17 00:00:00 2001 From: Anjali2019 Date: Tue, 25 Sep 2018 14:30:33 +0200 Subject: [PATCH 52/87] TST: Fixturize series/test_validate.py (#22756) --- pandas/tests/series/test_validate.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pandas/tests/series/test_validate.py b/pandas/tests/series/test_validate.py index a0cde5f81d021..8c4b6ee5b1d75 100644 --- a/pandas/tests/series/test_validate.py +++ b/pandas/tests/series/test_validate.py @@ -1,14 +1,7 @@ -from pandas.core.series import Series - import pytest import pandas.util.testing as tm -@pytest.fixture -def series(): - return Series([1, 2, 3, 4, 5]) - - class TestSeriesValidate(object): """Tests for error handling related to data types of method arguments.""" @@ -16,7 +9,7 @@ class TestSeriesValidate(object): "sort_values", "sort_index", "rename", "dropna"]) @pytest.mark.parametrize("inplace", [1, "True", [1, 2, 3], 5.0]) - def test_validate_bool_args(self, series, func, inplace): + def test_validate_bool_args(self, string_series, func, inplace): msg = "For argument \"inplace\" expected type bool" kwargs = dict(inplace=inplace) @@ -24,4 +17,4 @@ def test_validate_bool_args(self, series, func, inplace): kwargs["name"] = "hello" with tm.assert_raises_regex(ValueError, msg): - getattr(series, func)(**kwargs) + getattr(string_series, func)(**kwargs) From 2b5477e107ae1ff4090c43c269a0389dfac3560b Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 25 Sep 2018 08:46:47 -0400 Subject: [PATCH 53/87] Add Azure Pipelines badge to readme (#22828) --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3dde5e5e2a76e..bf90f76ae7bd1 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,14 @@ + + + + + Azure Pipelines build status + + + Coverage   From ea83ccc77e8b2e206168b41dfd93dcc68279b7ef Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Tue, 25 Sep 2018 08:48:40 -0400 Subject: [PATCH 54/87] DOC: remove appveyor badge from readme (#22829) --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index bf90f76ae7bd1..f26b9598bb5d3 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,6 @@ - - - - - appveyor build status - - - From 30b942a7b1a8628c0ce9a4931f83e3b31cd4965e Mon Sep 17 00:00:00 2001 From: Gosuke Shibahara Date: Tue, 25 Sep 2018 05:55:04 -0700 Subject: [PATCH 55/87] Fix DataFrame.to_string() justification (2) (#22505) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/io/formats/format.py | 5 +--- pandas/tests/io/formats/test_format.py | 32 ++++++++++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 6c91b6374b8af..c067adc8936a2 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -762,6 +762,7 @@ I/O - :func:`read_sas()` will correctly parse sas7bdat files with many columns (:issue:`22628`) - :func:`read_sas()` will correctly parse sas7bdat files with data page types having also bit 7 set (so page type is 128 + 256 = 384) (:issue:`16615`) - Bug in :meth:`detect_client_encoding` where potential ``IOError`` goes unhandled when importing in a mod_wsgi process due to restricted access to stdout. (:issue:`21552`) +- Bug in :func:`to_string()` that broke column alignment when ``index=False`` and width of first column's values is greater than the width of first column's header (:issue:`16839`, :issue:`13032`) Plotting ^^^^^^^^ diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index 1ff0613876838..db86409adc2b0 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -288,8 +288,7 @@ def to_string(self): if self.index: result = self.adj.adjoin(3, *[fmt_index[1:], fmt_values]) else: - result = self.adj.adjoin(3, fmt_values).replace('\n ', - '\n').strip() + result = self.adj.adjoin(3, fmt_values) if self.header and have_header: result = fmt_index[0] + '\n' + result @@ -650,8 +649,6 @@ def to_string(self): self._chk_truncate() strcols = self._to_str_columns() text = self.adj.adjoin(1, *strcols) - if not self.index: - text = text.replace('\n ', '\n').strip() self.buf.writelines(text) if self.should_show_dimensions: diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index ffbc978b92ba5..03e830fb09ad6 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -1269,18 +1269,42 @@ def test_to_string_specified_header(self): df.to_string(header=['X']) def test_to_string_no_index(self): - df = DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]}) + # GH 16839, GH 13032 + df = DataFrame({'x': [11, 22], 'y': [33, -44], 'z': ['AAA', ' ']}) df_s = df.to_string(index=False) - expected = "x y\n1 4\n2 5\n3 6" + # Leading space is expected for positive numbers. + expected = (" x y z\n" + " 11 33 AAA\n" + " 22 -44 ") + assert df_s == expected + df_s = df[['y', 'x', 'z']].to_string(index=False) + expected = (" y x z\n" + " 33 11 AAA\n" + "-44 22 ") assert df_s == expected def test_to_string_line_width_no_index(self): + # GH 13998, GH 22505 df = DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]}) df_s = df.to_string(line_width=1, index=False) - expected = "x \\\n1 \n2 \n3 \n\ny \n4 \n5 \n6" + expected = " x \\\n 1 \n 2 \n 3 \n\n y \n 4 \n 5 \n 6 " + + assert df_s == expected + + df = DataFrame({'x': [11, 22, 33], 'y': [4, 5, 6]}) + + df_s = df.to_string(line_width=1, index=False) + expected = " x \\\n 11 \n 22 \n 33 \n\n y \n 4 \n 5 \n 6 " + + assert df_s == expected + + df = DataFrame({'x': [11, 22, -33], 'y': [4, 5, -6]}) + + df_s = df.to_string(line_width=1, index=False) + expected = " x \\\n 11 \n 22 \n-33 \n\n y \n 4 \n 5 \n-6 " assert df_s == expected @@ -1793,7 +1817,7 @@ def test_to_string_without_index(self): # GH 11729 Test index=False option s = Series([1, 2, 3, 4]) result = s.to_string(index=False) - expected = (u('1\n') + '2\n' + '3\n' + '4') + expected = (u(' 1\n') + ' 2\n' + ' 3\n' + ' 4') assert result == expected def test_unicode_name_in_footer(self): From 1c4130d7834be9b762b41f9a9beaa1f34d9d272f Mon Sep 17 00:00:00 2001 From: Troels Nielsen Date: Tue, 25 Sep 2018 14:58:20 +0200 Subject: [PATCH 56/87] BUG: nlargest/nsmallest gave wrong result (#22752) (#22754) --- asv_bench/benchmarks/frame_methods.py | 13 ++++-- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/algorithms.py | 57 +++++++++++++++++---------- pandas/tests/frame/test_analytics.py | 18 +++++++++ 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/asv_bench/benchmarks/frame_methods.py b/asv_bench/benchmarks/frame_methods.py index 1819cfa2725db..f911d506b1f4f 100644 --- a/asv_bench/benchmarks/frame_methods.py +++ b/asv_bench/benchmarks/frame_methods.py @@ -505,14 +505,21 @@ class NSort(object): param_names = ['keep'] def setup(self, keep): - self.df = DataFrame(np.random.randn(1000, 3), columns=list('ABC')) + self.df = DataFrame(np.random.randn(100000, 3), + columns=list('ABC')) - def time_nlargest(self, keep): + def time_nlargest_one_column(self, keep): self.df.nlargest(100, 'A', keep=keep) - def time_nsmallest(self, keep): + 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) + def time_nsmallest_two_columns(self, keep): + self.df.nsmallest(100, ['A', 'B'], keep=keep) + class Describe(object): diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index c067adc8936a2..3b61fde77cb9f 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -819,6 +819,7 @@ Other - :meth:`~pandas.io.formats.style.Styler.background_gradient` now takes a ``text_color_threshold`` parameter to automatically lighten the text color based on the luminance of the background color. This improves readability with dark background colors without the need to limit the background colormap range. (:issue:`21258`) - Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`) - :meth:`~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`) +- :meth:`DataFrame.nlargest` and :meth:`DataFrame.nsmallest` now returns the correct n values when keep != 'all' also when tied on the first columns (:issue:`22752`) - :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly. - Logical operations ``&, |, ^`` between :class:`Series` and :class:`Index` will no longer raise ``ValueError`` (:issue:`22092`) - diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index d39e9e08e2947..e91cc8ec1e996 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -1214,41 +1214,56 @@ def get_indexer(current_indexer, other_indexer): indexer = Int64Index([]) for i, column in enumerate(columns): - # For each column we apply method to cur_frame[column]. - # If it is the last column in columns, or if the values - # returned are unique in frame[column] we save this index - # and break - # Otherwise we must save the index of the non duplicated values - # and set the next cur_frame to cur_frame filtered on all - # duplcicated values (#GH15297) + # If it's the last column or if we have the number of + # results desired we are done. + # Otherwise there are duplicates of the largest/smallest + # value and we need to look at the rest of the columns + # to determine which of the rows with the largest/smallest + # value in the column to keep. series = cur_frame[column] - values = getattr(series, method)(cur_n, keep=self.keep) is_last_column = len(columns) - 1 == i - if is_last_column or values.nunique() == series.isin(values).sum(): + values = getattr(series, method)( + cur_n, + keep=self.keep if is_last_column else 'all') - # Last column in columns or values are unique in - # series => values - # is all that matters + if is_last_column or len(values) <= cur_n: indexer = get_indexer(indexer, values.index) break - duplicated_filter = series.duplicated(keep=False) - duplicated = values[duplicated_filter] - non_duplicated = values[~duplicated_filter] - indexer = get_indexer(indexer, non_duplicated.index) + # Now find all values which are equal to + # the (nsmallest: largest)/(nlarrgest: smallest) + # from our series. + border_value = values == values[values.index[-1]] + + # Some of these values are among the top-n + # some aren't. + unsafe_values = values[border_value] + + # These values are definitely among the top-n + safe_values = values[~border_value] + indexer = get_indexer(indexer, safe_values.index) - # Must set cur frame to include all duplicated values - # to consider for the next column, we also can reduce - # cur_n by the current length of the indexer - cur_frame = cur_frame[series.isin(duplicated)] + # Go on and separate the unsafe_values on the remaining + # columns. + cur_frame = cur_frame.loc[unsafe_values.index] cur_n = n - len(indexer) frame = frame.take(indexer) # Restore the index on frame frame.index = original_index.take(indexer) - return frame + + # If there is only one column, the frame is already sorted. + if len(columns) == 1: + return frame + + ascending = method == 'nsmallest' + + return frame.sort_values( + columns, + ascending=ascending, + kind='mergesort') # ------- ## ---- # diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py index 52a52a1fd8752..baebf414969be 100644 --- a/pandas/tests/frame/test_analytics.py +++ b/pandas/tests/frame/test_analytics.py @@ -2095,6 +2095,24 @@ def test_n_all_dtypes(self, df_main_dtypes): df.nsmallest(2, list(set(df) - {'category_string', 'string'})) df.nlargest(2, list(set(df) - {'category_string', 'string'})) + @pytest.mark.parametrize('method,expected', [ + ('nlargest', + pd.DataFrame({'a': [2, 2, 2, 1], 'b': [3, 2, 1, 3]}, + index=[2, 1, 0, 3])), + ('nsmallest', + pd.DataFrame({'a': [1, 1, 1, 2], 'b': [1, 2, 3, 1]}, + index=[5, 4, 3, 0]))]) + def test_duplicates_on_starter_columns(self, method, expected): + # regression test for #22752 + + df = pd.DataFrame({ + 'a': [2, 2, 2, 1, 1, 1], + 'b': [1, 2, 3, 3, 2, 1] + }) + + result = getattr(df, method)(4, columns=['a', 'b']) + tm.assert_frame_equal(result, expected) + def test_n_identical_values(self): # GH15297 df = pd.DataFrame({'a': [1] * 5, 'b': [1, 2, 3, 4, 5]}) From 302d43a840797175eee04959b8918d3870722a0e Mon Sep 17 00:00:00 2001 From: Thierry Moisan Date: Tue, 25 Sep 2018 09:07:51 -0400 Subject: [PATCH 57/87] DOC: fix DataFrame.isin docstring and doctests (#22767) --- ci/doctests.sh | 2 +- pandas/core/frame.py | 78 ++++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/ci/doctests.sh b/ci/doctests.sh index 48774a1e4d00d..b3d7f6785815a 100755 --- a/ci/doctests.sh +++ b/ci/doctests.sh @@ -21,7 +21,7 @@ if [ "$DOCTEST" ]; then # DataFrame / Series docstrings pytest --doctest-modules -v pandas/core/frame.py \ - -k"-axes -combine -isin -itertuples -join -nlargest -nsmallest -nunique -pivot_table -quantile -query -reindex -reindex_axis -replace -round -set_index -stack -to_dict -to_stata" + -k"-axes -combine -itertuples -join -nlargest -nsmallest -nunique -pivot_table -quantile -query -reindex -reindex_axis -replace -round -set_index -stack -to_dict -to_stata" if [ $? -ne "0" ]; then RET=1 diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 81d5c112885ec..721c31c57bc06 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -7451,52 +7451,66 @@ def to_period(self, freq=None, axis=0, copy=True): def isin(self, values): """ - Return boolean DataFrame showing whether each element in the - DataFrame is contained in values. + Whether each element in the DataFrame is contained in values. Parameters ---------- - values : iterable, Series, DataFrame or dictionary + values : iterable, Series, DataFrame or dict The result will only be true at a location if all the labels match. If `values` is a Series, that's the index. If - `values` is a dictionary, the keys must be the column names, + `values` is a dict, the keys must be the column names, which must match. If `values` is a DataFrame, then both the index and column labels must match. Returns ------- + DataFrame + DataFrame of booleans showing whether each element in the DataFrame + is contained in values. - DataFrame of booleans + See Also + -------- + DataFrame.eq: Equality test for DataFrame. + Series.isin: Equivalent method on Series. + Series.str.contains: Test if pattern or regex is contained within a + string of a Series or Index. Examples -------- - When ``values`` is a list: - - >>> df = pd.DataFrame({'A': [1, 2, 3], 'B': ['a', 'b', 'f']}) - >>> df.isin([1, 3, 12, 'a']) - A B - 0 True True - 1 False False - 2 True False - - When ``values`` is a dict: - - >>> df = pd.DataFrame({'A': [1, 2, 3], 'B': [1, 4, 7]}) - >>> df.isin({'A': [1, 3], 'B': [4, 7, 12]}) - A B - 0 True False # Note that B didn't match the 1 here. - 1 False True - 2 True True - - When ``values`` is a Series or DataFrame: - - >>> df = pd.DataFrame({'A': [1, 2, 3], 'B': ['a', 'b', 'f']}) - >>> df2 = pd.DataFrame({'A': [1, 3, 3, 2], 'B': ['e', 'f', 'f', 'e']}) - >>> df.isin(df2) - A B - 0 True False - 1 False False # Column A in `df2` has a 3, but not at index 1. - 2 True True + + >>> df = pd.DataFrame({'num_legs': [2, 4], 'num_wings': [2, 0]}, + ... index=['falcon', 'dog']) + >>> df + num_legs num_wings + falcon 2 2 + dog 4 0 + + When ``values`` is a list check whether every value in the DataFrame + is present in the list (which animals have 0 or 2 legs or wings) + + >>> df.isin([0, 2]) + num_legs num_wings + falcon True True + dog False True + + When ``values`` is a dict, we can pass values to check for each + column separately: + + >>> df.isin({'num_wings': [0, 3]}) + num_legs num_wings + falcon False False + dog False True + + When ``values`` is a Series or DataFrame the index and column must + match. Note that 'falcon' does not match based on the number of legs + in df2. + + >>> other = pd.DataFrame({'num_legs': [8, 2],'num_wings': [0, 2]}, + ... index=['spider', 'falcon']) + >>> df.isin(other) + num_legs num_wings + falcon True True + dog False False """ if isinstance(values, dict): from pandas.core.reshape.concat import concat From 6b1f46052f0bd69f1a45989f1bfe650ba1c55ee1 Mon Sep 17 00:00:00 2001 From: gfyoung Date: Tue, 25 Sep 2018 09:38:53 -0700 Subject: [PATCH 58/87] ERR: Clarify location of EOF on unbalanced quotes (#22814) Closes gh-22789. --- pandas/_libs/src/parser/tokenizer.c | 2 +- pandas/io/parsers.py | 3 --- pandas/tests/io/parser/common.py | 14 -------------- pandas/tests/io/parser/quoting.py | 17 +++++++++++++++++ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pandas/_libs/src/parser/tokenizer.c b/pandas/_libs/src/parser/tokenizer.c index da0a9f7498aa8..2fce241027d56 100644 --- a/pandas/_libs/src/parser/tokenizer.c +++ b/pandas/_libs/src/parser/tokenizer.c @@ -1150,7 +1150,7 @@ static int parser_handle_eof(parser_t *self) { case IN_QUOTED_FIELD: self->error_msg = (char *)malloc(bufsize); snprintf(self->error_msg, bufsize, - "EOF inside string starting at line %lld", + "EOF inside string starting at row %lld", (long long)self->file_lines); return -1; diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py index 8d37bf4c84d5d..a4f1155117b12 100755 --- a/pandas/io/parsers.py +++ b/pandas/io/parsers.py @@ -2727,9 +2727,6 @@ def _next_iter_line(self, row_num): 'cannot be processed in Python\'s ' 'native csv library at the moment, ' 'so please pass in engine=\'c\' instead') - elif 'newline inside string' in msg: - msg = ('EOF inside string starting with ' - 'line ' + str(row_num)) if self.skipfooter > 0: reason = ('Error could possibly be due to ' diff --git a/pandas/tests/io/parser/common.py b/pandas/tests/io/parser/common.py index 064385e60c4ec..49e42786d6fb8 100644 --- a/pandas/tests/io/parser/common.py +++ b/pandas/tests/io/parser/common.py @@ -198,20 +198,6 @@ def test_malformed(self): 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 - pytest.raises(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') - assert len(df) == 3 - def test_unnamed_columns(self): data = """A,B,C,, 1,2,3,4,5 diff --git a/pandas/tests/io/parser/quoting.py b/pandas/tests/io/parser/quoting.py index 15427aaf9825c..013e635f80d21 100644 --- a/pandas/tests/io/parser/quoting.py +++ b/pandas/tests/io/parser/quoting.py @@ -9,6 +9,7 @@ import pandas.util.testing as tm from pandas import DataFrame +from pandas.errors import ParserError from pandas.compat import PY3, StringIO, u @@ -151,3 +152,19 @@ def test_quotechar_unicode(self): if PY3: result = self.read_csv(StringIO(data), quotechar=u('\u0001')) tm.assert_frame_equal(result, expected) + + def test_unbalanced_quoting(self): + # see gh-22789. + data = "a,b,c\n1,2,\"3" + + if self.engine == "c": + regex = "EOF inside string starting at row 1" + else: + regex = "unexpected end of data" + + with tm.assert_raises_regex(ParserError, regex): + self.read_csv(StringIO(data)) + + expected = DataFrame([[1, 2, 3]], columns=["a", "b", "c"]) + data = self.read_csv(StringIO(data + '"')) + tm.assert_frame_equal(data, expected) From a936399a539927eb74f06920f5e0a8a71b4cec56 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Tue, 25 Sep 2018 18:49:12 +0200 Subject: [PATCH 59/87] DOC: Updating str_pad docstring (#22570) --- pandas/core/strings.py | 48 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index ed091ce4956bc..861739f6c694c 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -1332,23 +1332,57 @@ def str_index(arr, sub, start=0, end=None, side='left'): def str_pad(arr, width, side='left', fillchar=' '): """ - Pad strings in the Series/Index with an additional character to - specified side. + Pad strings in the Series/Index up to width. Parameters ---------- width : int Minimum width of resulting string; additional characters will be filled - with spaces + with character defined in `fillchar`. side : {'left', 'right', 'both'}, default 'left' - fillchar : str - Additional character for filling, default is whitespace + Side from which to fill resulting string. + fillchar : str, default ' ' + Additional character for filling, default is whitespace. Returns ------- - padded : Series/Index of objects - """ + Series or Index of object + Returns Series or Index with minimum number of char in object. + + See Also + -------- + Series.str.rjust: Fills the left side of strings with an arbitrary + character. Equivalent to ``Series.str.pad(side='left')``. + Series.str.ljust: Fills the right side of strings with an arbitrary + character. Equivalent to ``Series.str.pad(side='right')``. + Series.str.center: Fills boths sides of strings with an arbitrary + character. Equivalent to ``Series.str.pad(side='both')``. + Series.str.zfill: Pad strings in the Series/Index by prepending '0' + character. Equivalent to ``Series.str.pad(side='left', fillchar='0')``. + + Examples + -------- + >>> s = pd.Series(["caribou", "tiger"]) + >>> s + 0 caribou + 1 tiger + dtype: object + + >>> s.str.pad(width=10) + 0 caribou + 1 tiger + dtype: object + >>> s.str.pad(width=10, side='right', fillchar='-') + 0 caribou--- + 1 tiger----- + dtype: object + + >>> s.str.pad(width=10, side='both', fillchar='-') + 0 -caribou-- + 1 --tiger--- + dtype: object + """ if not isinstance(fillchar, compat.string_types): msg = 'fillchar must be a character, not {0}' raise TypeError(msg.format(type(fillchar).__name__)) From 4a459b814e0200faeaaf70c175beb520a28bfa82 Mon Sep 17 00:00:00 2001 From: h-vetinari <33685575+h-vetinari@users.noreply.github.com> Date: Wed, 26 Sep 2018 12:05:37 +0200 Subject: [PATCH 60/87] Fixturize tests/frame/test_arithmetic (#22736) --- pandas/tests/frame/conftest.py | 18 +-- pandas/tests/frame/test_arithmetic.py | 193 ++++++++++---------------- 2 files changed, 84 insertions(+), 127 deletions(-) diff --git a/pandas/tests/frame/conftest.py b/pandas/tests/frame/conftest.py index fdedb93835d75..4a4ce4540b9d5 100644 --- a/pandas/tests/frame/conftest.py +++ b/pandas/tests/frame/conftest.py @@ -70,9 +70,10 @@ def mixed_float_frame(): Columns are ['A', 'B', 'C', 'D']. """ df = DataFrame(tm.getSeriesData()) - df.A = df.A.astype('float16') + df.A = df.A.astype('float32') df.B = df.B.astype('float32') - df.C = df.C.astype('float64') + df.C = df.C.astype('float16') + df.D = df.D.astype('float64') return df @@ -84,9 +85,10 @@ def mixed_float_frame2(): Columns are ['A', 'B', 'C', 'D']. """ df = DataFrame(tm.getSeriesData()) - df.D = df.D.astype('float16') + df.D = df.D.astype('float32') df.C = df.C.astype('float32') - df.B = df.B.astype('float64') + df.B = df.B.astype('float16') + df.D = df.D.astype('float64') return df @@ -99,10 +101,10 @@ def mixed_int_frame(): """ df = DataFrame({k: v.astype(int) for k, v in compat.iteritems(tm.getSeriesData())}) - df.A = df.A.astype('uint8') - df.B = df.B.astype('int32') - df.C = df.C.astype('int64') - df.D = np.ones(len(df.D), dtype='uint64') + df.A = df.A.astype('int32') + df.B = np.ones(len(df.B), dtype='uint64') + df.C = df.C.astype('uint8') + df.D = df.C.astype('int64') return df diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 9c61f13b944ea..2b08897864db0 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -4,8 +4,7 @@ import pytest import numpy as np -from pandas.compat import range, PY3 -import pandas.io.formats.printing as printing +from pandas.compat import range import pandas as pd import pandas.util.testing as tm @@ -127,132 +126,88 @@ def test_df_add_flex_filled_mixed_dtypes(self): 'B': ser * 2}) tm.assert_frame_equal(result, expected) - def test_arith_flex_frame(self): - seriesd = tm.getSeriesData() - frame = pd.DataFrame(seriesd).copy() - - mixed_float = pd.DataFrame({'A': frame['A'].copy().astype('float32'), - 'B': frame['B'].copy().astype('float32'), - 'C': frame['C'].copy().astype('float16'), - 'D': frame['D'].copy().astype('float64')}) - - intframe = pd.DataFrame({k: v.astype(int) - for k, v in seriesd.items()}) - mixed_int = pd.DataFrame({'A': intframe['A'].copy().astype('int32'), - 'B': np.ones(len(intframe), dtype='uint64'), - 'C': intframe['C'].copy().astype('uint8'), - 'D': intframe['D'].copy().astype('int64')}) - - # force these all to int64 to avoid platform testing issues - intframe = pd.DataFrame({c: s for c, s in intframe.items()}, - dtype=np.int64) - - ops = ['add', 'sub', 'mul', 'div', 'truediv', 'pow', 'floordiv', 'mod'] - if not PY3: - aliases = {} - else: - aliases = {'div': 'truediv'} - - for op in ops: - try: - alias = aliases.get(op, op) - f = getattr(operator, alias) - result = getattr(frame, op)(2 * frame) - exp = f(frame, 2 * frame) - tm.assert_frame_equal(result, exp) - - # vs mix float - result = getattr(mixed_float, op)(2 * mixed_float) - exp = f(mixed_float, 2 * mixed_float) - tm.assert_frame_equal(result, exp) - _check_mixed_float(result, dtype=dict(C=None)) - - # vs mix int - if op in ['add', 'sub', 'mul']: - result = getattr(mixed_int, op)(2 + mixed_int) - exp = f(mixed_int, 2 + mixed_int) - - # no overflow in the uint - dtype = None - if op in ['sub']: - dtype = dict(B='uint64', C=None) - elif op in ['add', 'mul']: - dtype = dict(C=None) - tm.assert_frame_equal(result, exp) - _check_mixed_int(result, dtype=dtype) - - # rops - r_f = lambda x, y: f(y, x) - result = getattr(frame, 'r' + op)(2 * frame) - exp = r_f(frame, 2 * frame) - tm.assert_frame_equal(result, exp) - - # vs mix float - result = getattr(mixed_float, op)(2 * mixed_float) - exp = f(mixed_float, 2 * mixed_float) - tm.assert_frame_equal(result, exp) - _check_mixed_float(result, dtype=dict(C=None)) - - result = getattr(intframe, op)(2 * intframe) - exp = f(intframe, 2 * intframe) - tm.assert_frame_equal(result, exp) - - # vs mix int - if op in ['add', 'sub', 'mul']: - result = getattr(mixed_int, op)(2 + mixed_int) - exp = f(mixed_int, 2 + mixed_int) - - # no overflow in the uint - dtype = None - if op in ['sub']: - dtype = dict(B='uint64', C=None) - elif op in ['add', 'mul']: - dtype = dict(C=None) - tm.assert_frame_equal(result, exp) - _check_mixed_int(result, dtype=dtype) - except: - printing.pprint_thing("Failing operation %r" % op) - raise - - # ndim >= 3 - ndim_5 = np.ones(frame.shape + (3, 4, 5)) + def test_arith_flex_frame(self, all_arithmetic_operators, float_frame, + mixed_float_frame): + # one instance of parametrized fixture + op = all_arithmetic_operators + + def f(x, y): + # r-versions not in operator-stdlib; get op without "r" and invert + if op.startswith('__r'): + return getattr(operator, op.replace('__r', '__'))(y, x) + return getattr(operator, op)(x, y) + + result = getattr(float_frame, op)(2 * float_frame) + exp = f(float_frame, 2 * float_frame) + tm.assert_frame_equal(result, exp) + + # vs mix float + result = getattr(mixed_float_frame, op)(2 * mixed_float_frame) + exp = f(mixed_float_frame, 2 * mixed_float_frame) + tm.assert_frame_equal(result, exp) + _check_mixed_float(result, dtype=dict(C=None)) + + @pytest.mark.parametrize('op', ['__add__', '__sub__', '__mul__']) + def test_arith_flex_frame_mixed(self, op, int_frame, mixed_int_frame, + mixed_float_frame): + f = getattr(operator, op) + + # vs mix int + result = getattr(mixed_int_frame, op)(2 + mixed_int_frame) + exp = f(mixed_int_frame, 2 + mixed_int_frame) + + # no overflow in the uint + dtype = None + if op in ['__sub__']: + dtype = dict(B='uint64', C=None) + elif op in ['__add__', '__mul__']: + dtype = dict(C=None) + tm.assert_frame_equal(result, exp) + _check_mixed_int(result, dtype=dtype) + + # vs mix float + result = getattr(mixed_float_frame, op)(2 * mixed_float_frame) + exp = f(mixed_float_frame, 2 * mixed_float_frame) + tm.assert_frame_equal(result, exp) + _check_mixed_float(result, dtype=dict(C=None)) + + # vs plain int + result = getattr(int_frame, op)(2 * int_frame) + exp = f(int_frame, 2 * int_frame) + tm.assert_frame_equal(result, exp) + + def test_arith_flex_frame_raise(self, all_arithmetic_operators, + float_frame): + # one instance of parametrized fixture + op = all_arithmetic_operators + + # Check that arrays with dim >= 3 raise + for dim in range(3, 6): + arr = np.ones((1,) * dim) msg = "Unable to coerce to Series/DataFrame" with tm.assert_raises_regex(ValueError, msg): - f(frame, ndim_5) + getattr(float_frame, op)(arr) - with tm.assert_raises_regex(ValueError, msg): - getattr(frame, op)(ndim_5) - - # res_add = frame.add(frame) - # res_sub = frame.sub(frame) - # res_mul = frame.mul(frame) - # res_div = frame.div(2 * frame) - - # tm.assert_frame_equal(res_add, frame + frame) - # tm.assert_frame_equal(res_sub, frame - frame) - # tm.assert_frame_equal(res_mul, frame * frame) - # tm.assert_frame_equal(res_div, frame / (2 * frame)) + def test_arith_flex_frame_corner(self, float_frame): - const_add = frame.add(1) - tm.assert_frame_equal(const_add, frame + 1) + const_add = float_frame.add(1) + tm.assert_frame_equal(const_add, float_frame + 1) # corner cases - result = frame.add(frame[:0]) - tm.assert_frame_equal(result, frame * np.nan) + result = float_frame.add(float_frame[:0]) + tm.assert_frame_equal(result, float_frame * np.nan) + + result = float_frame[:0].add(float_frame) + tm.assert_frame_equal(result, float_frame * np.nan) - result = frame[:0].add(frame) - tm.assert_frame_equal(result, frame * np.nan) with tm.assert_raises_regex(NotImplementedError, 'fill_value'): - frame.add(frame.iloc[0], fill_value=3) + float_frame.add(float_frame.iloc[0], fill_value=3) + with tm.assert_raises_regex(NotImplementedError, 'fill_value'): - frame.add(frame.iloc[0], axis='index', fill_value=3) - - def test_arith_flex_series(self): - arr = np.array([[1., 2., 3.], - [4., 5., 6.], - [7., 8., 9.]]) - df = pd.DataFrame(arr, columns=['one', 'two', 'three'], - index=['a', 'b', 'c']) + float_frame.add(float_frame.iloc[0], axis='index', fill_value=3) + + def test_arith_flex_series(self, simple_frame): + df = simple_frame row = df.xs('a') col = df['two'] From a393675b992c8b6f9acd8d0d586c008c111d4177 Mon Sep 17 00:00:00 2001 From: Shadi Akiki Date: Wed, 26 Sep 2018 13:20:20 +0300 Subject: [PATCH 61/87] ENH: correlation function accepts method being a callable (#22684) --- doc/source/computation.rst | 15 +++++++++++++ doc/source/whatsnew/v0.24.0.txt | 2 ++ pandas/core/frame.py | 20 +++++++++++++++-- pandas/core/nanops.py | 2 ++ pandas/core/series.py | 18 +++++++++++++-- pandas/tests/series/test_analytics.py | 32 +++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 4 deletions(-) diff --git a/doc/source/computation.rst b/doc/source/computation.rst index 5e7b8be5f8af0..0d2021de8f88e 100644 --- a/doc/source/computation.rst +++ b/doc/source/computation.rst @@ -153,6 +153,21 @@ Like ``cov``, ``corr`` also supports the optional ``min_periods`` keyword: frame.corr(min_periods=12) +.. versionadded:: 0.24.0 + +The ``method`` argument can also be a callable for a generic correlation +calculation. In this case, it should be a single function +that produces a single value from two ndarray inputs. Suppose we wanted to +compute the correlation based on histogram intersection: + +.. ipython:: python + + # histogram intersection + histogram_intersection = lambda a, b: np.minimum( + np.true_divide(a, a.sum()), np.true_divide(b, b.sum()) + ).sum() + frame.corr(method=histogram_intersection) + A related method :meth:`~DataFrame.corrwith` is implemented on DataFrame to compute the correlation between like-labeled Series contained in different DataFrame objects. diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3b61fde77cb9f..7eb3ea303cc9d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -20,6 +20,8 @@ New features - :func:`DataFrame.to_parquet` now accepts ``index`` as an argument, allowing the user to override the engine's default behavior to include or omit the dataframe's indexes from the resulting Parquet file. (:issue:`20768`) +- :meth:`DataFrame.corr` and :meth:`Series.corr` now accept a callable for generic calculation methods of correlation, e.g. histogram intersection (:issue:`22684`) + .. _whatsnew_0240.enhancements.extension_array_operators: diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 721c31c57bc06..e16f61d7f5f02 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -6672,10 +6672,14 @@ def corr(self, method='pearson', min_periods=1): Parameters ---------- - method : {'pearson', 'kendall', 'spearman'} + method : {'pearson', 'kendall', 'spearman'} or callable * pearson : standard correlation coefficient * kendall : Kendall Tau correlation coefficient * spearman : Spearman rank correlation + * callable: callable with input two 1d ndarrays + and returning a float + .. versionadded:: 0.24.0 + min_periods : int, optional Minimum number of observations required per pair of columns to have a valid result. Currently only available for pearson @@ -6684,6 +6688,18 @@ def corr(self, method='pearson', min_periods=1): Returns ------- y : DataFrame + + Examples + -------- + >>> import numpy as np + >>> histogram_intersection = lambda a, b: np.minimum(a, b + ... ).sum().round(decimals=1) + >>> df = pd.DataFrame([(.2, .3), (.0, .6), (.6, .0), (.2, .1)], + ... columns=['dogs', 'cats']) + >>> df.corr(method=histogram_intersection) + dogs cats + dogs 1.0 0.3 + cats 0.3 1.0 """ numeric_df = self._get_numeric_data() cols = numeric_df.columns @@ -6695,7 +6711,7 @@ def corr(self, method='pearson', min_periods=1): elif method == 'spearman': correl = libalgos.nancorr_spearman(ensure_float64(mat), minp=min_periods) - elif method == 'kendall': + elif method == 'kendall' or callable(method): if min_periods is None: min_periods = 1 mat = ensure_float64(mat).T diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py index f44fb4f6e9e14..7619d47cbc8f9 100644 --- a/pandas/core/nanops.py +++ b/pandas/core/nanops.py @@ -766,6 +766,8 @@ def nancorr(a, b, method='pearson', min_periods=None): def get_corr_func(method): if method in ['kendall', 'spearman']: from scipy.stats import kendalltau, spearmanr + elif callable(method): + return method def _pearson(a, b): return np.corrcoef(a, b)[0, 1] diff --git a/pandas/core/series.py b/pandas/core/series.py index fdb9ef59c1d3e..544a08981c83b 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -1910,10 +1910,14 @@ def corr(self, other, method='pearson', min_periods=None): Parameters ---------- other : Series - method : {'pearson', 'kendall', 'spearman'} + method : {'pearson', 'kendall', 'spearman'} or callable * pearson : standard correlation coefficient * kendall : Kendall Tau correlation coefficient * spearman : Spearman rank correlation + * callable: callable with input two 1d ndarray + and returning a float + .. versionadded:: 0.24.0 + min_periods : int, optional Minimum number of observations needed to have a valid result @@ -1921,12 +1925,22 @@ def corr(self, other, method='pearson', min_periods=None): Returns ------- correlation : float + + Examples + -------- + >>> import numpy as np + >>> histogram_intersection = lambda a, b: np.minimum(a, b + ... ).sum().round(decimals=1) + >>> s1 = pd.Series([.2, .0, .6, .2]) + >>> s2 = pd.Series([.3, .6, .0, .1]) + >>> s1.corr(s2, method=histogram_intersection) + 0.3 """ this, other = self.align(other, join='inner', copy=False) if len(this) == 0: return np.nan - if method in ['pearson', 'spearman', 'kendall']: + if method in ['pearson', 'spearman', 'kendall'] or callable(method): return nanops.nancorr(this.values, other.values, method=method, min_periods=min_periods) diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py index 9acd6501c3825..58a160d17cbe8 100644 --- a/pandas/tests/series/test_analytics.py +++ b/pandas/tests/series/test_analytics.py @@ -789,6 +789,38 @@ def test_corr_invalid_method(self): with tm.assert_raises_regex(ValueError, msg): s1.corr(s2, method="____") + def test_corr_callable_method(self): + # 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) + + # full overlap + tm.assert_almost_equal( + self.ts.corr(self.ts, method=my_corr), 1.) + + # partial overlap + tm.assert_almost_equal( + self.ts[:15].corr(self.ts[5:], method=my_corr), 1.) + + # No overlap + assert np.isnan( + self.ts[::2].corr(self.ts[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): # full overlap tm.assert_almost_equal(self.ts.cov(self.ts), self.ts.std() ** 2) From 739e6be23431ae5263ebbc0c35d30c0afd05951f Mon Sep 17 00:00:00 2001 From: William Ayd Date: Wed, 26 Sep 2018 05:35:54 -0500 Subject: [PATCH 62/87] Fixed Issue Preventing Agg on RollingGroupBy Objects (#21323) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/base.py | 4 +-- pandas/core/groupby/base.py | 9 +++++- pandas/tests/groupby/test_groupby.py | 10 ++++-- pandas/tests/test_window.py | 48 ++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 7eb3ea303cc9d..0e591e180e078 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -785,6 +785,7 @@ Groupby/Resample/Rolling - Bug in :meth:`Series.resample` when passing ``numpy.timedelta64`` to ``loffset`` kwarg (:issue:`7687`). - Bug in :meth:`Resampler.asfreq` when frequency of ``TimedeltaIndex`` is a subperiod of a new frequency (:issue:`13022`). - Bug in :meth:`SeriesGroupBy.mean` when values were integral but could not fit inside of int64, overflowing instead. (:issue:`22487`) +- :func:`RollingGroupby.agg` and :func:`ExpandingGroupby.agg` now support multiple aggregation functions as parameters (:issue:`15072`) Sparse ^^^^^^ diff --git a/pandas/core/base.py b/pandas/core/base.py index 26fea89b45ae1..7f14a68503973 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -245,8 +245,8 @@ def _obj_with_exclusions(self): def __getitem__(self, key): if self._selection is not None: - raise Exception('Column(s) {selection} already selected' - .format(selection=self._selection)) + raise IndexError('Column(s) {selection} already selected' + .format(selection=self._selection)) if isinstance(key, (list, tuple, ABCSeries, ABCIndexClass, np.ndarray)): diff --git a/pandas/core/groupby/base.py b/pandas/core/groupby/base.py index 96c74f7fd4d75..ac84971de08d8 100644 --- a/pandas/core/groupby/base.py +++ b/pandas/core/groupby/base.py @@ -44,8 +44,15 @@ def _gotitem(self, key, ndim, subset=None): # we need to make a shallow copy of ourselves # with the same groupby kwargs = {attr: getattr(self, attr) for attr in self._attributes} + + # Try to select from a DataFrame, falling back to a Series + try: + groupby = self._groupby[key] + except IndexError: + groupby = self._groupby + self = self.__class__(subset, - groupby=self._groupby[key], + groupby=groupby, parent=self, **kwargs) self._reset_cache() diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 483f814bc8383..3cdd0965ccfd0 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -623,8 +623,14 @@ def test_as_index_series_return_frame(df): assert isinstance(result2, DataFrame) assert_frame_equal(result2, expected2) - # corner case - pytest.raises(Exception, grouped['C'].__getitem__, 'D') + +def test_as_index_series_column_slice_raises(df): + # GH15072 + grouped = df.groupby('A', as_index=False) + msg = r"Column\(s\) C already selected" + + with tm.assert_raises_regex(IndexError, msg): + grouped['C'].__getitem__('D') def test_groupby_as_index_cython(df): diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index 052bfd2b858fb..cc663fc59cbf1 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from itertools import product import pytest import warnings @@ -314,6 +315,53 @@ def test_preserve_metadata(self): 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() + + 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) + + result = window.agg(OrderedDict(( + ('low', ['mean', 'max']), + ('high', ['mean', 'min']), + ))) + + tm.assert_frame_equal(result, expected) + @pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") class TestWindow(Base): From f64f9940265baeba3658fa5ad87f03d7d5ad25a8 Mon Sep 17 00:00:00 2001 From: HubertKl <39779339+HubertKl@users.noreply.github.com> Date: Wed, 26 Sep 2018 12:50:52 +0100 Subject: [PATCH 63/87] DOC: Updating Series.autocorr docstring (#22787) --- pandas/core/series.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/pandas/core/series.py b/pandas/core/series.py index 544a08981c83b..59fb019af9b1c 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2035,7 +2035,10 @@ def diff(self, periods=1): def autocorr(self, lag=1): """ - Lag-N autocorrelation + Compute the lag-N autocorrelation. + + This method computes the Pearson correlation between + the Series and its shifted self. Parameters ---------- @@ -2044,7 +2047,34 @@ def autocorr(self, lag=1): Returns ------- - autocorr : float + float + The Pearson correlation between self and self.shift(lag). + + See Also + -------- + Series.corr : Compute the correlation between two Series. + Series.shift : Shift index by desired number of periods. + DataFrame.corr : Compute pairwise correlation of columns. + DataFrame.corrwith : Compute pairwise correlation between rows or + columns of two DataFrame objects. + + Notes + ----- + If the Pearson correlation is not well defined return 'NaN'. + + Examples + -------- + >>> s = pd.Series([0.25, 0.5, 0.2, -0.05]) + >>> s.autocorr() + 0.1035526330902407 + >>> s.autocorr(lag=2) + -0.9999999999999999 + + If the Pearson correlation is not well defined, then 'NaN' is returned. + + >>> s = pd.Series([1, 0, 0, 0]) + >>> s.autocorr() + nan """ return self.corr(self.shift(lag)) From 9df8065ab3c1f0dd53d10185d40d7e49d00a92df Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 26 Sep 2018 09:27:52 -0500 Subject: [PATCH 64/87] Preserve Extension type on cross section (#22785) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/base.py | 6 ++-- pandas/core/frame.py | 11 ++++--- pandas/core/indexes/multi.py | 12 ++++--- pandas/core/internals/managers.py | 41 +++++++++++++++++------- pandas/tests/frame/test_dtypes.py | 12 +++++-- pandas/tests/indexing/test_indexing.py | 28 ++++++++++++++++ pandas/tests/indexing/test_multiindex.py | 4 +-- pandas/tests/series/test_dtypes.py | 8 ++--- 9 files changed, 91 insertions(+), 32 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 0e591e180e078..707257a35983e 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -500,6 +500,7 @@ ExtensionType Changes - :meth:`Series.combine()` works correctly with :class:`~pandas.api.extensions.ExtensionArray` inside of :class:`Series` (:issue:`20825`) - :meth:`Series.combine()` with scalar argument now works for any function type (:issue:`21248`) - :meth:`Series.astype` and :meth:`DataFrame.astype` now dispatch to :meth:`ExtensionArray.astype` (:issue:`21185:`). +- Slicing a single row of a ``DataFrame`` with multiple ExtensionArrays of the same type now preserves the dtype, rather than coercing to object (:issue:`22784`) - Added :meth:`pandas.api.types.register_extension_dtype` to register an extension type with pandas (:issue:`22664`) .. _whatsnew_0240.api.incompatibilities: diff --git a/pandas/core/base.py b/pandas/core/base.py index 7f14a68503973..00c049497c0d8 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -664,7 +664,7 @@ def transpose(self, *args, **kwargs): "definition self") @property - def _is_homogeneous(self): + def _is_homogeneous_type(self): """Whether the object has a single dtype. By definition, Series and Index are always considered homogeneous. @@ -673,8 +673,8 @@ def _is_homogeneous(self): See Also -------- - DataFrame._is_homogeneous - MultiIndex._is_homogeneous + DataFrame._is_homogeneous_type + MultiIndex._is_homogeneous_type """ return True diff --git a/pandas/core/frame.py b/pandas/core/frame.py index e16f61d7f5f02..cc58674398b70 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -614,7 +614,7 @@ def shape(self): return len(self.index), len(self.columns) @property - def _is_homogeneous(self): + def _is_homogeneous_type(self): """ Whether all the columns in a DataFrame have the same type. @@ -624,16 +624,17 @@ def _is_homogeneous(self): Examples -------- - >>> DataFrame({"A": [1, 2], "B": [3, 4]})._is_homogeneous + >>> DataFrame({"A": [1, 2], "B": [3, 4]})._is_homogeneous_type True - >>> DataFrame({"A": [1, 2], "B": [3.0, 4.0]})._is_homogeneous + >>> DataFrame({"A": [1, 2], "B": [3.0, 4.0]})._is_homogeneous_type False Items with the same type but different sizes are considered different types. - >>> DataFrame({"A": np.array([1, 2], dtype=np.int32), - ... "B": np.array([1, 2], dtype=np.int64)})._is_homogeneous + >>> DataFrame({ + ... "A": np.array([1, 2], dtype=np.int32), + ... "B": np.array([1, 2], dtype=np.int64)})._is_homogeneous_type False """ if self._data.any_extension_types: diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index ad38f037b6578..3e6b934e1e863 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -289,21 +289,23 @@ def levels(self): return self._levels @property - def _is_homogeneous(self): + def _is_homogeneous_type(self): """Whether the levels of a MultiIndex all have the same dtype. This looks at the dtypes of the levels. See Also -------- - Index._is_homogeneous - DataFrame._is_homogeneous + Index._is_homogeneous_type + DataFrame._is_homogeneous_type Examples -------- - >>> MultiIndex.from_tuples([('a', 'b'), ('a', 'c')])._is_homogeneous + >>> MultiIndex.from_tuples([ + ... ('a', 'b'), ('a', 'c')])._is_homogeneous_type True - >>> MultiIndex.from_tuples([('a', 1), ('a', 2)])._is_homogeneous + >>> MultiIndex.from_tuples([ + ... ('a', 1), ('a', 2)])._is_homogeneous_type False """ return len({x.dtype for x in self.levels}) <= 1 diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index 63738594799f5..2f29f1ae2509f 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -12,9 +12,6 @@ from pandas.util._validators import validate_bool_kwarg from pandas.compat import range, map, zip -from pandas.core.dtypes.dtypes import ( - ExtensionDtype, - PandasExtensionDtype) from pandas.core.dtypes.common import ( _NS_DTYPE, is_datetimelike_v_numeric, @@ -791,6 +788,11 @@ def _interleave(self): """ dtype = _interleaved_dtype(self.blocks) + if is_extension_array_dtype(dtype): + # TODO: https://github.com/pandas-dev/pandas/issues/22791 + # Give EAs some input on what happens here. Sparse needs this. + dtype = 'object' + result = np.empty(self.shape, dtype=dtype) if result.shape[0] == 0: @@ -906,14 +908,25 @@ def fast_xs(self, loc): # unique dtype = _interleaved_dtype(self.blocks) + n = len(items) - result = np.empty(n, dtype=dtype) + if is_extension_array_dtype(dtype): + # we'll eventually construct an ExtensionArray. + result = np.empty(n, dtype=object) + else: + result = np.empty(n, dtype=dtype) + for blk in self.blocks: # Such assignment may incorrectly coerce NaT to None # result[blk.mgr_locs] = blk._slice((slice(None), loc)) for i, rl in enumerate(blk.mgr_locs): result[rl] = blk._try_coerce_result(blk.iget((i, loc))) + if is_extension_array_dtype(dtype): + result = dtype.construct_array_type()._from_sequence( + result, dtype=dtype + ) + return result def consolidate(self): @@ -1855,16 +1868,22 @@ def _shape_compat(x): def _interleaved_dtype(blocks): - if not len(blocks): - return None + # type: (List[Block]) -> Optional[Union[np.dtype, ExtensionDtype]] + """Find the common dtype for `blocks`. - dtype = find_common_type([b.dtype for b in blocks]) + Parameters + ---------- + blocks : List[Block] - # only numpy compat - if isinstance(dtype, (PandasExtensionDtype, ExtensionDtype)): - dtype = np.object + Returns + ------- + dtype : Optional[Union[np.dtype, ExtensionDtype]] + None is returned when `blocks` is empty. + """ + if not len(blocks): + return None - return dtype + return find_common_type([b.dtype for b in blocks]) def _consolidate(blocks): diff --git a/pandas/tests/frame/test_dtypes.py b/pandas/tests/frame/test_dtypes.py index ca4bd64659e06..c91370dc36770 100644 --- a/pandas/tests/frame/test_dtypes.py +++ b/pandas/tests/frame/test_dtypes.py @@ -836,8 +836,16 @@ def test_constructor_list_str_na(self, string_dtype): "B": pd.Categorical(['b', 'c'])}), False), ]) - def test_is_homogeneous(self, data, expected): - assert data._is_homogeneous is expected + def test_is_homogeneous_type(self, data, expected): + assert data._is_homogeneous_type is expected + + def test_asarray_homogenous(self): + df = pd.DataFrame({"A": pd.Categorical([1, 2]), + "B": pd.Categorical([1, 2])}) + result = np.asarray(df) + # may change from object in the future + expected = np.array([[1, 1], [2, 2]], dtype='object') + tm.assert_numpy_array_equal(result, expected) class TestDataFrameDatetimeWithTZ(TestData): diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 761c633f89da3..0f524ca0aaac5 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -1079,3 +1079,31 @@ def test_validate_indices_high(): def test_validate_indices_empty(): with tm.assert_raises_regex(IndexError, "indices are out"): validate_indices(np.array([0, 1]), 0) + + +def test_extension_array_cross_section(): + # A cross-section of a homogeneous EA should be an EA + df = pd.DataFrame({ + "A": pd.core.arrays.integer_array([1, 2]), + "B": pd.core.arrays.integer_array([3, 4]) + }, index=['a', 'b']) + expected = pd.Series(pd.core.arrays.integer_array([1, 3]), + index=['A', 'B'], name='a') + result = df.loc['a'] + tm.assert_series_equal(result, expected) + + result = df.iloc[0] + tm.assert_series_equal(result, expected) + + +def test_extension_array_cross_section_converts(): + df = pd.DataFrame({ + "A": pd.core.arrays.integer_array([1, 2]), + "B": np.array([1, 2]), + }, index=['a', 'b']) + result = df.loc['a'] + expected = pd.Series([1, 1], dtype=object, index=['A', 'B'], name='a') + tm.assert_series_equal(result, expected) + + result = df.iloc[0] + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/indexing/test_multiindex.py b/pandas/tests/indexing/test_multiindex.py index aefa8badf72e7..b8f80164e5402 100644 --- a/pandas/tests/indexing/test_multiindex.py +++ b/pandas/tests/indexing/test_multiindex.py @@ -738,8 +738,8 @@ def test_multiindex_contains_dropped(self): (MultiIndex.from_product([(1, 2), (3, 4)]), True), (MultiIndex.from_product([('a', 'b'), (1, 2)]), False), ]) - def test_multiindex_is_homogeneous(self, data, expected): - assert data._is_homogeneous is expected + def test_multiindex_is_homogeneous_type(self, data, expected): + assert data._is_homogeneous_type is expected class TestMultiIndexSlicers(object): diff --git a/pandas/tests/series/test_dtypes.py b/pandas/tests/series/test_dtypes.py index 83a458eedbd93..125dff9ecfa7c 100644 --- a/pandas/tests/series/test_dtypes.py +++ b/pandas/tests/series/test_dtypes.py @@ -509,7 +509,7 @@ def test_infer_objects_series(self): assert actual.dtype == 'object' tm.assert_series_equal(actual, expected) - def test_is_homogeneous(self): - assert Series()._is_homogeneous - assert Series([1, 2])._is_homogeneous - assert Series(pd.Categorical([1, 2]))._is_homogeneous + 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 From a03d9535b16a6d5441334ef2e698d72778cf8115 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 26 Sep 2018 14:03:56 -0500 Subject: [PATCH 65/87] DOC: Fix warnings in doc build (#22838) --- doc/source/api.rst | 9 +++++++ doc/source/basics.rst | 2 +- doc/source/cookbook.rst | 6 ++--- doc/source/ecosystem.rst | 8 +++--- doc/source/io.rst | 29 ++++++++++----------- doc/source/text.rst | 5 ++-- doc/source/timeseries.rst | 45 +++++++++++++++++++-------------- doc/source/whatsnew/v0.18.0.txt | 2 +- doc/source/whatsnew/v0.20.0.txt | 2 +- doc/source/whatsnew/v0.24.0.txt | 9 ++++--- pandas/core/generic.py | 18 +++++++++---- pandas/core/series.py | 10 +++++--- pandas/core/window.py | 6 ++--- pandas/io/formats/style.py | 1 + 14 files changed, 88 insertions(+), 64 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index e4b055c14ec27..073ed8a082a11 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -2603,3 +2603,12 @@ objects. generated/pandas.Series.ix generated/pandas.Series.imag generated/pandas.Series.real + + +.. Can't convince sphinx to generate toctree for this class attribute. +.. So we do it manually to avoid a warning + +.. toctree:: + :hidden: + + generated/pandas.api.extensions.ExtensionDtype.na_value diff --git a/doc/source/basics.rst b/doc/source/basics.rst index c18b94fea9a28..6eeb97349100a 100644 --- a/doc/source/basics.rst +++ b/doc/source/basics.rst @@ -1935,7 +1935,7 @@ NumPy's type-system for a few cases. * :ref:`Categorical ` * :ref:`Datetime with Timezone ` * :ref:`Period ` -* :ref:`Interval ` +* :ref:`Interval ` Pandas uses the ``object`` dtype for storing strings. diff --git a/doc/source/cookbook.rst b/doc/source/cookbook.rst index f6fa9e9f86143..a4dc99383a562 100644 --- a/doc/source/cookbook.rst +++ b/doc/source/cookbook.rst @@ -505,13 +505,11 @@ Unlike agg, apply's callable is passed a sub-DataFrame which gives you access to .. ipython:: python df = pd.DataFrame({'A' : [1, 1, 2, 2], 'B' : [1, -1, 1, 2]}) - gb = df.groupby('A') def replace(g): - mask = g < 0 - g.loc[mask] = g[~mask].mean() - return g + mask = g < 0 + return g.where(mask, g[~mask].mean()) gb.transform(replace) diff --git a/doc/source/ecosystem.rst b/doc/source/ecosystem.rst index 1014982fea21a..7fffcadd8ee8c 100644 --- a/doc/source/ecosystem.rst +++ b/doc/source/ecosystem.rst @@ -73,8 +73,8 @@ large data to thin clients. `seaborn `__ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Seaborn is a Python visualization library based on `matplotlib -`__. It provides a high-level, dataset-oriented +Seaborn is a Python visualization library based on +`matplotlib `__. It provides a high-level, dataset-oriented interface for creating attractive statistical graphics. The plotting functions in seaborn understand pandas objects and leverage pandas grouping operations internally to support concise specification of complex visualizations. Seaborn @@ -140,7 +140,7 @@ which are utilized by Jupyter Notebook for displaying (Note: HTML tables may or may not be compatible with non-HTML Jupyter output formats.) -See :ref:`Options and Settings ` and :ref:`` +See :ref:`Options and Settings ` and :ref:`options.available ` for pandas ``display.`` settings. `quantopian/qgrid `__ @@ -169,7 +169,7 @@ or the clipboard into a new pandas DataFrame via a sophisticated import wizard. Most pandas classes, methods and data attributes can be autocompleted in Spyder's `Editor `__ and `IPython Console `__, -and Spyder's `Help pane`__ can retrieve +and Spyder's `Help pane `__ can retrieve and render Numpydoc documentation on pandas objects in rich text with Sphinx both automatically and on-demand. diff --git a/doc/source/io.rst b/doc/source/io.rst index cb22bb9198e25..039cba2993381 100644 --- a/doc/source/io.rst +++ b/doc/source/io.rst @@ -66,16 +66,13 @@ The pandas I/O API is a set of top level ``reader`` functions accessed like CSV & Text files ---------------- -The two workhorse functions for reading text files (a.k.a. flat files) are -:func:`read_csv` and :func:`read_table`. They both use the same parsing code to -intelligently convert tabular data into a ``DataFrame`` object. See the -:ref:`cookbook` for some advanced strategies. +The workhorse function for reading text files (a.k.a. flat files) is +:func:`read_csv`. See the :ref:`cookbook` for some advanced strategies. Parsing options ''''''''''''''' -The functions :func:`read_csv` and :func:`read_table` accept the following -common arguments: +:func:`read_csv` accepts the following common arguments: Basic +++++ @@ -780,8 +777,8 @@ Date Handling Specifying Date Columns +++++++++++++++++++++++ -To better facilitate working with datetime data, :func:`read_csv` and -:func:`read_table` use the keyword arguments ``parse_dates`` and ``date_parser`` +To better facilitate working with datetime data, :func:`read_csv` +uses the keyword arguments ``parse_dates`` and ``date_parser`` to allow users to specify a variety of columns and date/time formats to turn the input text data into ``datetime`` objects. @@ -1434,7 +1431,7 @@ Suppose you have data indexed by two columns: print(open('data/mindex_ex.csv').read()) -The ``index_col`` argument to ``read_csv`` and ``read_table`` can take a list of +The ``index_col`` argument to ``read_csv`` can take a list of column numbers to turn multiple columns into a ``MultiIndex`` for the index of the returned object: @@ -1505,8 +1502,8 @@ class of the csv module. For this, you have to specify ``sep=None``. .. ipython:: python - print(open('tmp2.sv').read()) - pd.read_csv('tmp2.sv', sep=None, engine='python') + print(open('tmp2.sv').read()) + pd.read_csv('tmp2.sv', sep=None, engine='python') .. _io.multiple_files: @@ -1528,16 +1525,16 @@ rather than reading the entire file into memory, such as the following: .. ipython:: python print(open('tmp.sv').read()) - table = pd.read_table('tmp.sv', sep='|') + table = pd.read_csv('tmp.sv', sep='|') table -By specifying a ``chunksize`` to ``read_csv`` or ``read_table``, the return +By specifying a ``chunksize`` to ``read_csv``, the return value will be an iterable object of type ``TextFileReader``: .. ipython:: python - reader = pd.read_table('tmp.sv', sep='|', chunksize=4) + reader = pd.read_csv('tmp.sv', sep='|', chunksize=4) reader for chunk in reader: @@ -1548,7 +1545,7 @@ Specifying ``iterator=True`` will also return the ``TextFileReader`` object: .. ipython:: python - reader = pd.read_table('tmp.sv', sep='|', iterator=True) + reader = pd.read_csv('tmp.sv', sep='|', iterator=True) reader.get_chunk(5) .. ipython:: python @@ -3067,7 +3064,7 @@ Clipboard A handy way to grab data is to use the :meth:`~DataFrame.read_clipboard` method, which takes the contents of the clipboard buffer and passes them to the -``read_table`` method. For instance, you can copy the following text to the +``read_csv`` method. For instance, you can copy the following text to the clipboard (CTRL-C on many operating systems): .. code-block:: python diff --git a/doc/source/text.rst b/doc/source/text.rst index 61583a179e572..d01c48695d0d6 100644 --- a/doc/source/text.rst +++ b/doc/source/text.rst @@ -312,14 +312,15 @@ All one-dimensional list-likes can be combined in a list-like container (includi s u - s.str.cat([u.values, ['A', 'B', 'C', 'D'], map(str, u.index)], na_rep='-') + s.str.cat([u.values, + u.index.astype(str).values], na_rep='-') All elements must match in length to the calling ``Series`` (or ``Index``), except those having an index if ``join`` is not None: .. ipython:: python v - s.str.cat([u, v, ['A', 'B', 'C', 'D']], join='outer', na_rep='-') + s.str.cat([u, v], join='outer', na_rep='-') If using ``join='right'`` on a list of ``others`` that contains different indexes, the union of these indexes will be used as the basis for the final concatenation: diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 71bc064ffb0c2..85b0abe421eb2 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -753,18 +753,28 @@ regularity will result in a ``DatetimeIndex``, although frequency is lost: Iterating through groups ------------------------ -With the :ref:`Resampler` object in hand, iterating through the grouped data is very +With the ``Resampler`` object in hand, iterating through the grouped data is very natural and functions similarly to :py:func:`itertools.groupby`: .. ipython:: python - resampled = df.resample('H') + small = pd.Series( + range(6), + index=pd.to_datetime(['2017-01-01T00:00:00', + '2017-01-01T00:30:00', + '2017-01-01T00:31:00', + '2017-01-01T01:00:00', + '2017-01-01T03:00:00', + '2017-01-01T03:05:00']) + ) + resampled = small.resample('H') for name, group in resampled: - print(name) - print(group) + print("Group: ", name) + print("-" * 27) + print(group, end="\n\n") -See :ref:`groupby.iterating-label`. +See :ref:`groupby.iterating-label` or :class:`Resampler.__iter__` for more. .. _timeseries.components: @@ -910,26 +920,22 @@ It's definitely worth exploring the ``pandas.tseries.offsets`` module and the various docstrings for the classes. These operations (``apply``, ``rollforward`` and ``rollback``) preserve time -(hour, minute, etc) information by default. To reset time, use ``normalize=True`` -when creating the offset instance. If ``normalize=True``, the result is -normalized after the function is applied. - +(hour, minute, etc) information by default. To reset time, use ``normalize`` +before or after applying the operation (depending on whether you want the +time information included in the operation. .. ipython:: python + ts = pd.Timestamp('2014-01-01 09:00') day = Day() - day.apply(pd.Timestamp('2014-01-01 09:00')) - - day = Day(normalize=True) - day.apply(pd.Timestamp('2014-01-01 09:00')) + day.apply(ts) + day.apply(ts).normalize() + ts = pd.Timestamp('2014-01-01 22:00') hour = Hour() - hour.apply(pd.Timestamp('2014-01-01 22:00')) - - hour = Hour(normalize=True) - hour.apply(pd.Timestamp('2014-01-01 22:00')) - hour.apply(pd.Timestamp('2014-01-01 23:00')) - + hour.apply(ts) + hour.apply(ts).normalize() + hour.apply(pd.Timestamp("2014-01-01 23:30")).normalize() .. _timeseries.dayvscalendarday: @@ -1488,6 +1494,7 @@ time. The method for this is :meth:`~Series.shift`, which is available on all of the pandas objects. .. ipython:: python + ts = pd.Series(range(len(rng)), index=rng) ts = ts[:5] ts.shift(1) diff --git a/doc/source/whatsnew/v0.18.0.txt b/doc/source/whatsnew/v0.18.0.txt index a3213136d998a..e38ba54d4b058 100644 --- a/doc/source/whatsnew/v0.18.0.txt +++ b/doc/source/whatsnew/v0.18.0.txt @@ -373,7 +373,7 @@ New Behavior: s = pd.Series([1,2,3], index=np.arange(3.)) s s.index - print(s.to_csv(path=None)) + print(s.to_csv(path_or_buf=None, header=False)) Changes to dtype assignment behaviors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 3c0818343208a..9f5fbdc195f34 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -186,7 +186,7 @@ Previously, only ``gzip`` compression was supported. By default, compression of URLs and paths are now inferred using their file extensions. Additionally, support for bz2 compression in the python 2 C-engine improved (:issue:`14874`). -.. ipython:: python +.. code-block:: python url = 'https://github.com/{repo}/raw/{branch}/{path}'.format( repo = 'pandas-dev/pandas', diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 707257a35983e..481c31d2410a9 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -253,7 +253,6 @@ UTC offset (:issue:`17697`, :issue:`11736`, :issue:`22457`) .. code-block:: ipython - In [2]: pd.to_datetime("2015-11-18 15:30:00+05:30") Out[2]: Timestamp('2015-11-18 10:00:00') @@ -291,6 +290,7 @@ Passing ``utc=True`` will mimic the previous behavior but will correctly indicat that the dates have been converted to UTC .. ipython:: python + pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True) .. _whatsnew_0240.api_breaking.calendarday: @@ -457,7 +457,7 @@ Previous Behavior: Out[3]: Int64Index([0, 1, 2], dtype='int64') -.. _whatsnew_0240.api.timedelta64_subtract_nan +.. _whatsnew_0240.api.timedelta64_subtract_nan: Addition/Subtraction of ``NaN`` from :class:`DataFrame` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -468,9 +468,10 @@ all-``NaT``. This is for compatibility with ``TimedeltaIndex`` and ``Series`` behavior (:issue:`22163`) .. ipython:: python + :okexcept: - df = pd.DataFrame([pd.Timedelta(days=1)]) - df - np.nan + df = pd.DataFrame([pd.Timedelta(days=1)]) + df - np.nan Previous Behavior: diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 19ac4b49358d4..393e7caae5fab 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -2060,10 +2060,12 @@ def to_json(self, path_or_buf=None, orient=None, date_format=None, like. .. versionadded:: 0.19.0 - compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None}, - default 'infer' + + compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None} + A string representing the compression to use in the output file, - only used when the first argument is a filename. + only used when the first argument is a filename. By default, the + compression is inferred from the filename. .. versionadded:: 0.21.0 .. versionchanged:: 0.24.0 @@ -9514,7 +9516,9 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, a string. .. versionchanged:: 0.24.0 - Was previously named "path" for Series. + + Was previously named "path" for Series. + sep : str, default ',' String of length 1. Field delimiter for the output file. na_rep : str, default '' @@ -9528,7 +9532,9 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, assumed to be aliases for the column names. .. versionchanged:: 0.24.0 - Previously defaulted to False for Series. + + Previously defaulted to False for Series. + index : bool, default True Write row names (index). index_label : str or sequence, or False, default None @@ -9550,7 +9556,9 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, compression). .. versionchanged:: 0.24.0 + 'infer' option added and set to default. + quoting : optional constant from csv module Defaults to csv.QUOTE_MINIMAL. If you have set a `float_format` then floats are converted to strings and thus csv.QUOTE_NONNUMERIC diff --git a/pandas/core/series.py b/pandas/core/series.py index 59fb019af9b1c..83f80c305c5eb 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2065,10 +2065,10 @@ def autocorr(self, lag=1): Examples -------- >>> s = pd.Series([0.25, 0.5, 0.2, -0.05]) - >>> s.autocorr() - 0.1035526330902407 - >>> s.autocorr(lag=2) - -0.9999999999999999 + >>> s.autocorr() # doctest: +ELLIPSIS + 0.10355... + >>> s.autocorr(lag=2) # doctest: +ELLIPSIS + -0.99999... If the Pearson correlation is not well defined, then 'NaN' is returned. @@ -2789,6 +2789,7 @@ def nlargest(self, n=5, keep='first'): keep : {'first', 'last', 'all'}, default 'first' When there are duplicate values that cannot all fit in a Series of `n` elements: + - ``first`` : take the first occurrences based on the index order - ``last`` : take the last occurrences based on the index order - ``all`` : keep all occurrences. This can result in a Series of @@ -2884,6 +2885,7 @@ def nsmallest(self, n=5, keep='first'): keep : {'first', 'last', 'all'}, default 'first' When there are duplicate values that cannot all fit in a Series of `n` elements: + - ``first`` : take the first occurrences based on the index order - ``last`` : take the last occurrences based on the index order - ``all`` : keep all occurrences. This can result in a Series of diff --git a/pandas/core/window.py b/pandas/core/window.py index 66f48f403c941..5cdf62d5a5537 100644 --- a/pandas/core/window.py +++ b/pandas/core/window.py @@ -1404,7 +1404,7 @@ def _get_cov(X, Y): otherwise defaults to `False`. Not relevant for :class:`~pandas.Series`. **kwargs - Under Review. + Unused. Returns ------- @@ -1430,7 +1430,7 @@ def _get_cov(X, Y): all 1's), except for :class:`~pandas.DataFrame` inputs with `pairwise` set to `True`. - Function will return `NaN`s for correlations of equal valued sequences; + Function will return ``NaN`` for correlations of equal valued sequences; this is the result of a 0/0 division error. When `pairwise` is set to `False`, only matching columns between `self` and @@ -1446,7 +1446,7 @@ def _get_cov(X, Y): Examples -------- The below example shows a rolling calculation with a window size of - four matching the equivalent function call using `numpy.corrcoef`. + four matching the equivalent function call using :meth:`numpy.corrcoef`. >>> v1 = [3, 3, 3, 5, 8] >>> v2 = [3, 4, 4, 4, 8] diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b175dd540a518..f4bb53ba4f218 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1073,6 +1073,7 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100, percent of the cell's width. align : {'left', 'zero',' mid'}, default 'left' How to align the bars with the cells. + - 'left' : the min value starts at the left of the cell. - 'zero' : a value of zero is located at the center of the cell. - 'mid' : the center of the cell is at (max-min)/2, or From 7343fd37f809feeb98844a34dc5854a08df2aa94 Mon Sep 17 00:00:00 2001 From: Thiviyan Thanapalasingam Date: Wed, 26 Sep 2018 20:41:11 +0100 Subject: [PATCH 66/87] DOC: Handle whitespace in pathnames (#20880) --- doc/make.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/make.py b/doc/make.py index d85747458148d..cab5fa0ed4c52 100755 --- a/doc/make.py +++ b/doc/make.py @@ -233,10 +233,10 @@ def _sphinx_build(self, kind): '-b{}'.format(kind), '-{}'.format( 'v' * self.verbosity) if self.verbosity else '', - '-d{}'.format(os.path.join(BUILD_PATH, 'doctrees')), + '-d"{}"'.format(os.path.join(BUILD_PATH, 'doctrees')), '-Dexclude_patterns={}'.format(self.exclude_patterns), - SOURCE_PATH, - os.path.join(BUILD_PATH, kind)) + '"{}"'.format(SOURCE_PATH), + '"{}"'.format(os.path.join(BUILD_PATH, kind))) def _open_browser(self): base_url = os.path.join('file://', DOC_PATH, 'build', 'html') From a85140102691ae675a942da6673edd5d95fa9b06 Mon Sep 17 00:00:00 2001 From: Eric Boxer Date: Thu, 27 Sep 2018 08:54:11 -0400 Subject: [PATCH 67/87] DOC iteritems docstring update and examples (#22658) --- pandas/core/frame.py | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index cc58674398b70..0d0e186b15c54 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -779,14 +779,50 @@ def style(self): return Styler(self) def iteritems(self): - """ + r""" Iterator over (column name, Series) pairs. - See also + Iterates over the DataFrame columns, returning a tuple with the column name + and the content as a Series. + + Yields + ------ + label : object + The column names for the DataFrame being iterated over. + content : Series + The column entries belonging to each label, as a Series. + + See Also -------- - iterrows : Iterate over DataFrame rows as (index, Series) pairs. - itertuples : Iterate over DataFrame rows as namedtuples of the values. + DataFrame.iterrows : Iterate over DataFrame rows as (index, Series) pairs. + DataFrame.itertuples : Iterate over DataFrame rows as namedtuples of the values. + Examples + -------- + >>> df = pd.DataFrame({'species': ['bear', 'bear', 'marsupial'], + ... 'population': [1864, 22000, 80000]}, + ... index=['panda', 'polar', 'koala']) + >>> df + species population + panda bear 1864 + polar bear 22000 + koala marsupial 80000 + >>> for label, content in df.iteritems(): + ... print('label:', label) + ... print('content:', content, sep='\n') + ... + label: species + content: + panda bear + polar bear + koala marsupial + Name: species, dtype: object + label: population + content: + panda 1864 + polar 22000 + koala 80000 + Name: population, dtype: int64 """ if self.columns.is_unique and hasattr(self, '_item_cache'): for k in self.columns: From d115900f4f80d2f9016b41041bde1564980415b3 Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Thu, 27 Sep 2018 14:15:58 -0400 Subject: [PATCH 68/87] STYLE: lint --- pandas/core/frame.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 0d0e186b15c54..d5b273f37a3a2 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -782,8 +782,8 @@ def iteritems(self): r""" Iterator over (column name, Series) pairs. - Iterates over the DataFrame columns, returning a tuple with the column name - and the content as a Series. + Iterates over the DataFrame columns, returning a tuple with + the column name and the content as a Series. Yields ------ @@ -794,8 +794,10 @@ def iteritems(self): See Also -------- - DataFrame.iterrows : Iterate over DataFrame rows as (index, Series) pairs. - DataFrame.itertuples : Iterate over DataFrame rows as namedtuples of the values. + DataFrame.iterrows : Iterate over DataFrame rows as + (index, Series) pairs. + DataFrame.itertuples : Iterate over DataFrame rows as namedtuples + of the values. Examples -------- From e45a6c14fc969c31c46ae890e72883ffe6658b4e Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Fri, 28 Sep 2018 10:06:15 -0500 Subject: [PATCH 69/87] COMPAT: mpl 3.0 (#22870) * COMPAT: mpl 3.0 * faster test --- doc/source/whatsnew/v0.24.0.txt | 2 ++ pandas/plotting/_compat.py | 1 + pandas/plotting/_core.py | 10 ++++++++-- pandas/tests/plotting/common.py | 1 + pandas/tests/plotting/test_datetimelike.py | 8 ++++++-- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 481c31d2410a9..3e1711edb0f27 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -193,6 +193,8 @@ Other Enhancements - :meth:`Series.resample` and :meth:`DataFrame.resample` have gained the :meth:`Resampler.quantile` (:issue:`15023`). - :meth:`Index.to_frame` now supports overriding column name(s) (:issue:`22580`). - New attribute :attr:`__git_version__` will return git commit sha of current build (:issue:`21295`). +- Compatibility with Matplotlib 3.0 (:issue:`22790`). + .. _whatsnew_0240.api_breaking: Backwards incompatible API changes diff --git a/pandas/plotting/_compat.py b/pandas/plotting/_compat.py index 46ebd4217862d..5032b259e9831 100644 --- a/pandas/plotting/_compat.py +++ b/pandas/plotting/_compat.py @@ -29,3 +29,4 @@ def inner(): _mpl_ge_2_0_1 = _mpl_version('2.0.1', operator.ge) _mpl_ge_2_1_0 = _mpl_version('2.1.0', operator.ge) _mpl_ge_2_2_0 = _mpl_version('2.2.0', operator.ge) +_mpl_ge_3_0_0 = _mpl_version('3.0.0', operator.ge) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 4fa3b51c60ee4..77c97412bd3d7 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -32,7 +32,8 @@ from pandas.plotting._compat import (_mpl_ge_1_3_1, _mpl_ge_1_5_0, - _mpl_ge_2_0_0) + _mpl_ge_2_0_0, + _mpl_ge_3_0_0) from pandas.plotting._style import (plot_params, _get_standard_colors) from pandas.plotting._tools import (_subplots, _flatten, table, @@ -843,11 +844,16 @@ def _plot_colorbar(self, ax, **kwds): # For a more detailed description of the issue # see the following link: # https://github.com/ipython/ipython/issues/11215 - img = ax.collections[0] cbar = self.fig.colorbar(img, ax=ax, **kwds) + + if _mpl_ge_3_0_0(): + # The workaround below is no longer necessary. + return + points = ax.get_position().get_points() cbar_points = cbar.ax.get_position().get_points() + cbar.ax.set_position([cbar_points[0, 0], points[0, 1], cbar_points[1, 0] - cbar_points[0, 0], diff --git a/pandas/tests/plotting/common.py b/pandas/tests/plotting/common.py index 09687dd97bd43..5c88926828fa6 100644 --- a/pandas/tests/plotting/common.py +++ b/pandas/tests/plotting/common.py @@ -57,6 +57,7 @@ def setup_method(self, method): self.mpl_ge_2_0_0 = plotting._compat._mpl_ge_2_0_0() self.mpl_ge_2_0_1 = plotting._compat._mpl_ge_2_0_1() self.mpl_ge_2_2_0 = plotting._compat._mpl_ge_2_2_0() + self.mpl_ge_3_0_0 = plotting._compat._mpl_ge_3_0_0() if self.mpl_ge_1_4_0: self.bp_n_objects = 7 diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 0abe82d138e5e..de6f6b931987c 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -151,7 +151,7 @@ def test_high_freq(self): freaks = ['ms', 'us'] for freq in freaks: _, ax = self.plt.subplots() - rng = date_range('1/1/2012', periods=100000, freq=freq) + rng = date_range('1/1/2012', periods=100, freq=freq) ser = Series(np.random.randn(len(rng)), rng) _check_plot_works(ser.plot, ax=ax) @@ -1492,7 +1492,11 @@ def test_matplotlib_scatter_datetime64(self): ax.scatter(x="time", y="y", data=df) fig.canvas.draw() label = ax.get_xticklabels()[0] - assert label.get_text() == '2017-12-12' + 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): From f771ef67e8fa3f395967d5c02918fbbc3410612a Mon Sep 17 00:00:00 2001 From: gfyoung Date: Sat, 29 Sep 2018 12:55:51 -0700 Subject: [PATCH 70/87] BLD: Drop nonexistent dependency of _libs/parsers (#22883) Follow-up to gh-22469. Closes gh-22831. --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2aca048dcd4fb..bfd0c50c9e9be 100755 --- a/setup.py +++ b/setup.py @@ -544,8 +544,7 @@ def srcpath(name=None, suffix='.pyx', subdir='src'): '_libs.parsers': { 'pyxfile': '_libs/parsers', 'depends': ['pandas/_libs/src/parser/tokenizer.h', - 'pandas/_libs/src/parser/io.h', - 'pandas/_libs/src/numpy_helper.h'], + 'pandas/_libs/src/parser/io.h'], 'sources': ['pandas/_libs/src/parser/tokenizer.c', 'pandas/_libs/src/parser/io.c']}, '_libs.reduction': { From f849134a0af2a6a1cc016f61c68ba24d08b8a9a3 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Sun, 30 Sep 2018 05:22:36 +0100 Subject: [PATCH 71/87] Updating `DataFrame.mode` docstring. (#22404) --- pandas/core/frame.py | 74 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index d5b273f37a3a2..b4e8b4e3a6bec 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -7303,38 +7303,82 @@ def _get_agg_axis(self, axis_num): def mode(self, axis=0, numeric_only=False, dropna=True): """ - Gets the mode(s) of each element along the axis selected. Adds a row - for each mode per label, fills in gaps with nan. + Get the mode(s) of each element along the selected axis. - Note that there could be multiple values returned for the selected - axis (when more than one item share the maximum frequency), which is - the reason why a dataframe is returned. If you want to impute missing - values with the mode in a dataframe ``df``, you can just do this: - ``df.fillna(df.mode().iloc[0])`` + The mode of a set of values is the value that appears most often. + It can be multiple values. Parameters ---------- axis : {0 or 'index', 1 or 'columns'}, default 0 + The axis to iterate over while searching for the mode: + * 0 or 'index' : get mode of each column * 1 or 'columns' : get mode of each row - numeric_only : boolean, default False - if True, only apply to numeric columns - dropna : boolean, default True + numeric_only : bool, default False + If True, only apply to numeric columns. + dropna : bool, default True Don't consider counts of NaN/NaT. .. versionadded:: 0.24.0 Returns ------- - modes : DataFrame (sorted) + DataFrame + The modes of each column or row. + + See Also + -------- + Series.mode : Return the highest frequency value in a Series. + Series.value_counts : Return the counts of values in a Series. Examples -------- - >>> df = pd.DataFrame({'A': [1, 2, 1, 2, 1, 2, 3]}) + >>> df = pd.DataFrame([('bird', 2, 2), + ... ('mammal', 4, np.nan), + ... ('arthropod', 8, 0), + ... ('bird', 2, np.nan)], + ... index=('falcon', 'horse', 'spider', 'ostrich'), + ... columns=('species', 'legs', 'wings')) + >>> df + species legs wings + falcon bird 2 2.0 + horse mammal 4 NaN + spider arthropod 8 0.0 + ostrich bird 2 NaN + + By default, missing values are not considered, and the mode of wings + are both 0 and 2. The second row of species and legs contains ``NaN``, + because they have only one mode, but the DataFrame has two rows. + >>> df.mode() - A - 0 1 - 1 2 + species legs wings + 0 bird 2.0 0.0 + 1 NaN NaN 2.0 + + Setting ``dropna=False`` ``NaN`` values are considered and they can be + the mode (like for wings). + + >>> df.mode(dropna=False) + species legs wings + 0 bird 2 NaN + + Setting ``numeric_only=True``, only the mode of numeric columns is + computed, and columns of other types are ignored. + + >>> df.mode(numeric_only=True) + legs wings + 0 2.0 0.0 + 1 NaN 2.0 + + To compute the mode over columns and not rows, use the axis parameter: + + >>> df.mode(axis='columns', numeric_only=True) + 0 1 + falcon 2.0 NaN + horse 4.0 NaN + spider 0.0 8.0 + ostrich 2.0 NaN """ data = self if not numeric_only else self._get_numeric_data() From 14598c6f82973b33d8b55e1e493f6103f9e16a62 Mon Sep 17 00:00:00 2001 From: MatanCohe <30339529+MatanCohe@users.noreply.github.com> Date: Sun, 30 Sep 2018 09:48:40 +0300 Subject: [PATCH 72/87] STYLE: Fix linting of benchmarks (#22886) Fixed the following: * asv_bench/benchmarks/algorithms.py:12:5: E722 do not use bare except' * asv_bench/benchmarks/timeseries.py:1:1: F401 'warnings' imported but unused * asv_bench/benchmarks/stat_ops.py:21:9: E722 do not use bare except' * asv_bench/benchmarks/stat_ops.py:59:9: E722 do not use bare except' * asv_bench/benchmarks/pandas_vb_common.py:5:1: F401 'pandas.Panel' imported but unused * asv_bench/benchmarks/pandas_vb_common.py:12:5: E722 do not use bare except' * asv_bench/benchmarks/pandas_vb_common.py:37:9: E722 do not use bare except' * asv_bench/benchmarks/join_merge.py:32:9: E722 do not use bare except' * asv_bench/benchmarks/io/csv.py:2:1: F401 'timeit' imported but unused * asv_bench/benchmarks/io/csv.py:8:1: F401 'pandas.compat.PY2' imported but unused * asv_bench/benchmarks/io/csv.py:184:80: E501 line too long (87 > 79 characters) --- asv_bench/benchmarks/algorithms.py | 2 +- asv_bench/benchmarks/io/csv.py | 6 ++---- asv_bench/benchmarks/join_merge.py | 2 +- asv_bench/benchmarks/pandas_vb_common.py | 5 ++--- asv_bench/benchmarks/stat_ops.py | 4 ++-- asv_bench/benchmarks/timeseries.py | 1 - 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/asv_bench/benchmarks/algorithms.py b/asv_bench/benchmarks/algorithms.py index cccd38ef11251..fc34440ece2ed 100644 --- a/asv_bench/benchmarks/algorithms.py +++ b/asv_bench/benchmarks/algorithms.py @@ -9,7 +9,7 @@ try: hashing = import_module(imp) break - except: + except (ImportError, TypeError, ValueError): pass from .pandas_vb_common import setup # noqa diff --git a/asv_bench/benchmarks/io/csv.py b/asv_bench/benchmarks/io/csv.py index 2d4bdc7ae812a..12cb893462b87 100644 --- a/asv_bench/benchmarks/io/csv.py +++ b/asv_bench/benchmarks/io/csv.py @@ -1,11 +1,9 @@ import random -import timeit 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 PY2 from pandas.compat import cStringIO as StringIO from ..pandas_vb_common import setup, BaseIO # noqa @@ -181,8 +179,8 @@ def time_read_csv(self, sep, decimal, float_precision): 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')) + read_csv(self.data(self.StringIO_input), sep=sep, header=None, + engine='python', float_precision=None, names=list('abc')) class ReadCSVCategorical(BaseIO): diff --git a/asv_bench/benchmarks/join_merge.py b/asv_bench/benchmarks/join_merge.py index de0a3b33da147..7487a0d8489b7 100644 --- a/asv_bench/benchmarks/join_merge.py +++ b/asv_bench/benchmarks/join_merge.py @@ -29,7 +29,7 @@ def setup(self): try: with warnings.catch_warnings(record=True): self.mdf1.consolidate(inplace=True) - except: + except (AttributeError, TypeError): pass self.mdf2 = self.mdf1.copy() self.mdf2.index = self.df2.index diff --git a/asv_bench/benchmarks/pandas_vb_common.py b/asv_bench/benchmarks/pandas_vb_common.py index e255cd94f265b..e7b25d567e03b 100644 --- a/asv_bench/benchmarks/pandas_vb_common.py +++ b/asv_bench/benchmarks/pandas_vb_common.py @@ -2,14 +2,13 @@ from importlib import import_module import numpy as np -from pandas import Panel # 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, @@ -34,7 +33,7 @@ def remove(self, f): """Remove created files""" try: os.remove(f) - except: + except OSError: # On Windows, attempting to remove a file that is in use # causes an exception to be raised pass diff --git a/asv_bench/benchmarks/stat_ops.py b/asv_bench/benchmarks/stat_ops.py index c447c78d0d070..ecfcb27806f54 100644 --- a/asv_bench/benchmarks/stat_ops.py +++ b/asv_bench/benchmarks/stat_ops.py @@ -18,7 +18,7 @@ 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: + except TypeError: from pandas.core import nanops nanops._USE_BOTTLENECK = use_bottleneck self.df_func = getattr(df, op) @@ -56,7 +56,7 @@ 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: + except TypeError: from pandas.core import nanops nanops._USE_BOTTLENECK = use_bottleneck self.s_func = getattr(s, op) diff --git a/asv_bench/benchmarks/timeseries.py b/asv_bench/benchmarks/timeseries.py index 2c98cc1659519..2557ba7672a0e 100644 --- a/asv_bench/benchmarks/timeseries.py +++ b/asv_bench/benchmarks/timeseries.py @@ -1,4 +1,3 @@ -import warnings from datetime import timedelta import numpy as np From f4fae353eaaa719db335ec2b21259932de30f46d Mon Sep 17 00:00:00 2001 From: Ming Li <14131823+minggli@users.noreply.github.com> Date: Sun, 30 Sep 2018 14:59:46 +0100 Subject: [PATCH 73/87] BLD: minor break ci/requirements-optional-pip.txt (#22889) --- ci/requirements-optional-pip.txt | 4 ++-- scripts/convert_deps.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ci/requirements-optional-pip.txt b/ci/requirements-optional-pip.txt index 2e1bf0ca22bcf..09ce8e59a3b46 100644 --- a/ci/requirements-optional-pip.txt +++ b/ci/requirements-optional-pip.txt @@ -14,7 +14,7 @@ lxml matplotlib nbsphinx numexpr -openpyxl=2.5.5 +openpyxl==2.5.5 pyarrow pymysql tables @@ -28,4 +28,4 @@ statsmodels xarray xlrd xlsxwriter -xlwt +xlwt \ No newline at end of file diff --git a/scripts/convert_deps.py b/scripts/convert_deps.py index aabeb24a0c3c8..3ff157e0a0d7b 100755 --- a/scripts/convert_deps.py +++ b/scripts/convert_deps.py @@ -1,6 +1,7 @@ """ Convert the conda environment.yaml to a pip requirements.txt """ +import re import yaml exclude = {'python=3'} @@ -15,6 +16,7 @@ required = dev['dependencies'] required = [rename.get(dep, dep) for dep in required if dep not in exclude] optional = [rename.get(dep, dep) for dep in optional if dep not in exclude] +optional = [re.sub("(?<=[^<>])=", '==', dep) for dep in optional] with open("ci/requirements_dev.txt", 'wt') as f: From 2f1b842119bc4d5242b587b62bde71d8f7ef19f8 Mon Sep 17 00:00:00 2001 From: Kang Yoosam Date: Mon, 1 Oct 2018 06:27:17 +0900 Subject: [PATCH 74/87] DOC: Fixes to the Japanese version of the cheatsheet (#22657) --- doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf | Bin 0 -> 206153 bytes doc/cheatsheet/Pandas_Cheat_Sheet_JA.pptx | Bin 0 -> 76495 bytes doc/cheatsheet/Pandas_Cheat_Sheet_JP.pdf | Bin 205542 -> 0 bytes doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx | Bin 105265 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JA.pptx delete mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JP.pdf delete mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf b/doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..daa65a944e68acfc9ce5e4b47929d806ae6ba41b GIT binary patch literal 206153 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�ci(J|R{ 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
1Dvm>^3fCy4Q$aJo|lBt|rbomhcma08#`% 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 bk2yFx0Y2al*9kC;i;ErY-n~ciVrc&d3i$UZ literal 0 HcmV?d00001 diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_JA.pptx b/doc/cheatsheet/Pandas_Cheat_Sheet_JA.pptx new file mode 100644 index 0000000000000000000000000000000000000000..6270a71e20ee88f5020a76f139f404f18f4bfc30 GIT binary patch literal 76495 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

Ql*HwboAFVY{v2e%FIi z;#B&SzJe8p!1s(8g2UGd&p$}XL2G7Z)Dicnt;{-7^wkEgmF|RdjEIgY z_=kBIB0uan6sb@*lhfwBw>Uf*wS>B_#lF$=8S!?n{h90lkqSr(jl3(R%C}1G9Yz7X zEYYurcytfTJM!(S?VxJQE5UiSkiv63fhg;32ku z_J`@SJRB-o^v@S0(B`&XjyA`e%|Pj7)Qyw?^GCHoY@mtEJ3Q#Ilqhe^!4OwQ1bX~+(3s{Af;>rspE zF^nbIQaC|&Y5jKETEN^QfB^&6zK4vl6jb@|=e^+IWAq=@KXd?fK_MIM)fjbkeF(dT zfdxc;XbCNf;h0|xcBb$vH;7)A`QfS3dMFOk(0NQ7DNWW{wU&QW#@Ed`n!6G^cG?bY zdou16l3y@&#J9n1N{-BK3tUH3>&NinTIxb}Bj?(j567{2iAovmG^EQ}#U{|Rxb-l~ zY7^Z`svFoythn{d>qJuoa;+5KWT~^y6Qn7{DH!R@Wa9qKTuxLNq~LP`oN3%%!o#oE zXLDNb)rVbU(%3+Aocq}6R!%vgj#3y_%dr1BV!~<#xYPXmwoK9QzR+NNs8W2r|DvzR z5^-^8jr^ku*N~i^3!Ca%VSulfR4-I&tj`p0h=WFTlA?mrt;^aJY`QvO^i9gUfGqc6 z@csYkop=5cSCIW7xcdHh=Kl8t*Z)bf{BP|Mx>F2feh99;+n;FU*K%uTWuqP4f0$-5 zGYNMjYwa}eBgmSo^0!=RYIj79E||wQul>_uwW{4K<@D#gtX!p>vl7ABIfc(U$QZLD z9>U240x@QBV#%>*lb4)tf76F%?BQDZp=Fg~os8XzLL!wDDz&-)0XlQL|@BI@c_&JESU9;}aH@ zyCWePD!jvZv5=?(w!vL-;t=fIA>;_#{kBz(MJP)OH>j4^Q*iOc6)ki?`y~toOtkuj z<>$A8YDp{Qixt@{kwBY^-V8kM@8OJWr+F&4A&O(+mB(8B_Ij4~$8GPd=oq(v|115t zO4amG&0>uFGVM8 z>6ov?y!`u2Ce$At4`pdlsrLPjGir+B))*X(VeTA^2DP(~Y4PFc=_c-VeO+#cYT`I|b zuVBM>YQN|-MAZp{Lph*Z=9yi&h|6gfa1?EwX4mnL^%A`-ZTKf&bW+&B&r~!wgT8AQkc#8{zfH_y~0n4_)bx4?W}FF5v)paFghZt@gc_6 z{oAxZ5B(u#&~h~-#-gf z{(^NmsK}p3>EtJ`&KY(QTB&@l9u3z*uv`H}4v&Qm-Yf=7E^*c5VPo(4kt9wl$fbH! z3R50#8Eid!!v6+J`NyS{x$XU5!7ifbs(Ig?85*uG$PDr85j0zomYbt=d@uMnY)zBO z5G}^3r5fw)1=2i#QjcNKLr#Iv1II5Exq+odFau#sX;g&F5PE=B(^4v2$&?83OJMDj z9DK5-`4CyBnwimCV)_afO6{ z`2W7JTCOPP#W%BADsPq>loMJLvKn+*E~8j$haxB&OcD6cBpAA5GLZ@}Cv*XeUm43^ z4k@N0NP>Z2XGE3svwutcn7I8O;H~f>PG9qzB}KL)O`TbMc6-ue^Bcb&{$?8p&EEVW zwPfKt$*?u?Tx@Xh^*oqcaI-zRU1C@7q-*Lp0!gUtLkwDlX>yzUwgcmA7%3{ZRO`F+3^0Q=s3psENiH5?WRiNb+-0 zYGEiX?$PndBhzGbm{nG?@vWrpF^WdB>x;y0|7X9hh?&mk=jWFn4_&A2xjHi$^b>LX}5eF)|QEV@Xod|bWP0!Y0reu zDsgssE2DuPL>dFuG3?I#eS5GuLwpe?v@!C~)t((gvB=#XRrHC;Y`60^R4`caTYi$xED1wz+wvtP8(U9PI(^>ZTseCB1qTj36pjgo^+F6*oO8$w(f}axK4MF#x$@^5NYH3dpQgc8 zi$Nr>2xBZt=1b{rO%r*#+&%4^8(g)@Sq;7dp`nWr|S>G(acz76! ztq5OMwQJ4&D(-%a>SJNZwMJ&nLFy9per-0ZwKi}0;G-}6XI@ruT%1x3!`s69HxZR~_`UjCRjnH=p?FtDEH9!47{gvwwQs`7(#Rd1TGmk%%*Ju(4Np z)Tf7r#Apo(lv$9knXXK2)=!V0no0crW=_L6bM4U2#X&DL&(UsY6v4Jlf|j^Cx)8BD z3-DY~tNj&%buiGMIyKtIy>lqCsC9JuvWVp}%OY1tUE1*FGiBRnI9Ef&d^I#>ij~s) z8(1;x+8+ktiG=TaUT{28m)}h-ZDuVaxPVf8H)!jWQJjsf%XyZ9e)aHUp}zuMwPprOzoiKDaZo zqrt<(v#;@^UEe>|xh5uEXLSZkryvQU2qPh%SjkQcybNO*Ny3FAut%-dlCTY3P2Oar zQN#uhf*bYR8|sA07vg*!~;o+_D=K6d_Yk66K5 z%+etq_1?u#Lo#%AhT5#k%ED<{%r5C}#m1Mg2-yf(mW6xJ@>HZpdyxss2V{67OSGD`?pY> zpz5vWcfJi4Hj9go0PMLbxtq7^2}2hbJ#C4#dJ$kyx}Zr=+u6p%g2)R=-3|mG3!7cE z56L#V_$pDhz>p#2C`;JXvzewg)(}u`iWKqpgF8D1UF;+@=Yi-%ia;&>!iazOROn{2 zpLZrgk(rqg2;mBJ|6Gyj)rQ1K#h&@RyuUBBavq5wiOv5LsvP`@zj_Q0y&y)yiK{(*U&%bbZie0=J(BH;Z`v~^>% zF^a+iA;8|8`heI+h@gmN_TGEtq4DD{7H!RrU4tT^~$i@msGJ(hs}SjRl96 z6~QM`228(j&mMgsxZan4t)S?&&WkGeADy$S=2vS+T_K!ma8>JMW$+!mgBuf!4w>Cw z-?Yn6ZtMC8s_ypg@9efAe7-%NbNRchsfj9KV`M1xf|amHM2W>a=ysaxo(HwCr&g&o zbiNH3F(}8v*Bx54(0OF`-3cMU$l!A$IoS%K;9r)faN(%%+2Ug+B_&y3q2(YseKm6} z?O+C~2w93~w-9=tTFZ6j9~%d9RXJyU=-ld14wW#)TJ-KIkm)7yVPRV9V}J4Oc5HI9 zuoT6LkiIpdwBYj6yrxmiYpvT>Z^+ZaXg{*D%S)A`q&@S4WF;prtaW$a(DT49doM#A zcOt0Tf81W2&ha2(rf*nD$;aX4y?T;=>`YPZ590l@8+1DysjgLsbx&)&Q#Od?VJ>S? zS(^D6m`DlP8B#~Rj<@*ab`j|sXnS96 z`_!?nJV&o6z){4v`*eKu?}i6-#&3W^^AVQwyE&B6R&VQ{%}Behs{yb-qVf~6o`EM( z3U)YQJQSWEC#KVSChjiitk7>nc|{o76?2V1v@GC&F{78%Mj5}7#b0Cc!@@6{mIUQd z*z|ODlc4PkR^hC<4}fBx)E)VbYS#-poIn6m4~zXraYv#gK9@{o`um;vOiy!gA{|aZ|KIFV|>mT8QwW_gA*f=B6I| zMW4`KeliEj2(4wDVP6FdaxX)08=P=yI}x0cT4Q3FU(~|kQ7;D@@V&cBe7BDO%bhdZ zmHKUbC!o3mRRpu5WPiS0-U|4D(1nc=f~xMG5EvaBu}_YV&ToHsdkCQy3>*seExlLx za^bhZRAVBTAqD7f&?N~p9c{jELhEjah&O{QqV4ZoM+K+u^EHBs}=N@Xi)ja27k+&oK>7 z`-^24Qrm-4P~ntiJjFW`sIv6fmoX&%0EAlGOSH%W5#%~z?Q(|Jg%ynyFz#+^KH?wp zNoH|7*Nc+Fg21gAs*YUvys>IKCDMF=hfp7PO&bP(bcphNfyYD6i{3K;F;&bNX6 zMZ#nF6CpL+ME$2o{}{)+lI!1_5^0`~szdm$u=%eZ%6AU%Q4kz^;S6a(2UVHedlzw` zZq&Qjv2e`64V(3D^NR6QK#5Rst(_YBUhIOBZh3xZcV?99gFtl#&3N8fmtDyrGve-G ze^nfu86e6r38*tq2#C@-TOFM5nJ<=@D&-+sR50!vL==lcE$<)+wa)a*S@pu1n1Xq} z5bW2u7GJ)~pQVP@q3fgUD&&GjXLkea`EJ^X-au}wf!TTbMLvaea8$x9fgwkLssIj# zy;R#l&iL`l(@&HuQ2fkhpxoN&1ZvdlDt4F8_9YuXy6y1UOIQh94a$7sBTY=JjK)eO7I)6lGduGTUx+3$<8;4i0 zeM?(j&exX#>WN=z%UAR0{%kyKxxan)AkXFYGo$&(o2oxmgbI=2fJ1Sk4yatqSP%wz zWq(cmN#BkPVh>Tsdx{ zkb{)?j)+`Fuvc5~$R^wxSA=_bM4p9l#rkK0JlD*X5x(MY?xkFrLrXS9YPi3UUuu5D zq#Z8HyA{Rm|Lr1Umt3$E&+ulYyQ{;~W*1OqZS!w>dRpCOJC!@J2W-mNXS{D@1)p{`Hd@7=PTKZXw&(!p_L(HXtwqub`np_cy1Dn@?>p>pB>O2#?h9xs*Iu(kR_e9{C3Z zm6+U=lw@QZt}HBDI=BmQXR+w+npMwz&-PW2 zzj=)0xcF50!ze%;JlbG1C?t6PF^N=Yz*!8&3gD6|W8JI%!wDSnCFTAVx{`!ug~3ss z3D4oJN8eKza><`8*5-3uHS<7COiH++i%&^_v zQXF-@p^=@KPKm{vwFPCz7&+8rlg+9jMnro;7cITUt^w~XB2uG1Pu88YyV<5S@C?bu zh>e9nTS1*f@-of@QWhDUPzR-l4C~0_r_@L!>{u!Ttllc1hyr0{8U}9qxV4^6m{L~Q zw>9+q=wsaP-tcQEh<@PE@jDe(&d%B9d>OAmUS9EPK_%**dTi0B+81@b-y$P&&El%y zw~)6`U;R^HPnrrP{zG#|Q~~s$xdgWu1JU3MB;Hd18?i*G;dfXDdo2Z0PxgxNY8Te{wo+rkEj1rp}Vqn{q zJ+SWYyfABQlg}T_Id4@V)01fvL`e~*kI(|)tzM7QOixMP~H zlb1@Wfv`B-?~9DoM?i+^cQocSL4Jg&=Svqdk%>Z|U0j$Cc2@v{rszQ!xXNeKh{7RT zz#~^C?>J=sh8jhQw$1r=>14V0_8lP%`5$t6Fj%^7+<0r$#3NF%pPtWM5TMTG?59h7 zK<0dUEmz-ToX$3huV_|Ct?CIUDlpoa=Gv2Yrsf+%utIur69Nu82{xgvH;98g%JY8h z#-EWg57RF9OPud>197uupUu5NH9*fq9^Ta~h&vbk?O{^eprz353N67BWe5NA%+%~H zGA)d1`7LXvR{P<)UjNF$euTHTFss7X60DAu6?qPul;l=AQ)EG5Z!wwf{RpUV6V+lw zbGHy@zY&1vZpgYQi?uGv!{z4TKysudRjq08ZX$HH)Q1W-F2qPj-eV*&&d8VJ!%?Bs)b+hrAC{}uPQyH-j|gN30Qd0Cxf|F z1HCwOr?oZg#Z1Y$e`J!zT20NCs6e`z*2}2st3iEz?hs}XFT4z$bbTa7BZyqWKblRh zhz2ZGQwHJ0!f`dGW}pmVDSZgqzznXM)6_=1`@cCp4{lwhcFhW39f=?5ixo7{C>yjQ zTAasc0E39x)C^W7U7eS;j(l0m>BTN@clzO;AZ0*?6)Nu_q|<9{1ElOEttFcnZ92C{ zMGpr9dnO$ zxxl+4KOE`7FD2yOcVFo)fmhjziUaC^?KTMJn4T@W^gc_BAOElIldg1l92Rop}QpcDsR8oCR z+rBJ2jWe#*%9v~O#)tg)sY;@YeQP|y>v-?}^rZsiEa%%QAA9=zNyTZLZW(C8 z@1ljphb>Lhjp%vAYYnz!YPUM{l%$)bxPbJ9I<0Dw(lKIqR`%mGFV}o(*M(gax+&~g z><$0)OgW4Wl$N;O?#FJfs6keZz`NMMT?R zimvV}#cOPn40e>tLE71sUbbD17yZoY1P2%7Zkm}(N)6};(?$RR=OphJYD@ z|CVM?x{(I0mC)9t)d_xYZU*6L^T7&S%A+gvx#uvn5=h$%Hu!~g3oKYkaJPdmU0)BX zp13)GE}-Z|ub2Q{5}z-}Pq#dBoABb)&C$*Pw~MhV1fXjQM=Wz8IlKZO@&*d|vJ= z+|wY#9TJ?X^hLh2_f0(1J9d{Lxf(9F-Qt}=%O^q0Ju|V7Iz1U|0ejF)yI4a*QLic= z3aBBg23XwzLYqyPSgfu|99C^|tX?>rK=p(<*4^yy>Gw$&0gG0RdHmw=xTp6Z!hjNx zP=b-S?}c?7gWO(1Bt$Mx9IRd!15Br^>Em^GqaN(cS%65o3$tt1OF*c+CWFC2j;Qz^ zKUtTSSENyrGH1sF1N3r`u4&*8XckQPlz`V&VEoACK(lYGg5w#$dFb6h9e71nT5WX8 z8d%Lwy{fyJVeJ_pOi-5x!x)uAP@-k^IOPfY%WuV>hw3S`Y{Dt=4t@CH44rI-`i}rX7H(4n0%ib zf>>k5d^7$5DxUx+_S7mA-m>|__)v5Zwl24OAQs_6fQ5y{_Gz%zCVYzDl0Sjf;<8Mt zjqo~`(&!@IV&`?L%n+$Ik_eM_zC2vZhi|_ zj-0V8o=Jk8N5||khSh|~B&Ny)&ipg!q=fvv6Bov%nDd40Z;P)7%&-AU%aN{fg7xQTY<aS?h#KAf_dEk^^DWXF3TBu8;aweM$+51yR zqQ|Ab*?47K>4I@dx&F*Q-n1b;hH}Q_*+Z;&nNWhTEoRd^?>9L);^RUSJm4t?!X(2L z@nX#*UJ>FQ%+D@VNBT{7CL8$7gn^aF4 zEuz-QX7Y0HOxmh$DSW~sq^RqV%hK8bqg8lL86$}-hOOqfeYr({1u&DsRE__3=DQNP zhZ1Q?WWViK7MCXJRp;A3miTI_Xa>L4t!zd2qx=?Rm}xgB@9x2)?U zB*YZk>U>r#_oYKmUdi6tHK$r-eCLU-Rl(FvU@Q4 z^l4>fDeyxR+bvKtwHq6*DF5>UPdN?s+z4!Dsr6`(O3dL~X|a0Qq<6pnS80=|O}XE? zt$%YDqc!3N2m@v5dpFkRXAf<^e~ zmQR2Q0SiCm2hCBjB`DLYnf1{NvS zF_(Ig<$?*Ostg7ZYhcG5Gev^XEM|Il9?VyVfd+R@4Q_fu943N{aEng@yTSSG@FKLo zTL=Yy3l1JnoPo70nm!N?r!mH8K>rSeMgm7S_++Jig^5ZFT2Q!9=yA;{9CB*Eh2eCE ze>iz#nO!7ue<4}!Wx&D(|Km_eBtM=LE4b+lF+P<6>Z(FCr2ys4iGsqGGNob1^nPmw zz>v=59B44muMLl zjDa>b1`2Ne6EQEXs0(L2ypfkB!G(*p1Iof|F@V#|vw_;}OfU;Xl1t^hzmYx@3P&_wBLAD#~df|e(J`fCn;PBkQwjU)e7Gx(KzEZHLY zgcC68!48=5&_{&tX_wG(pvYVHv>$_WJ;|D2$}G1F&Mh>QqOvl~&8=RCEA^4wq|JKl z7>|OhZ5$&D_C$P^D(ij9^t|d<7|(>>(=r4EL3+CX}{@qZZZeaS>FfSjmbB-X`?y2vl1*r>nKa{Le8P=>=|wXj!?x`tgco5rHv`U+qwmq!vH zjrhr=@L`;?eD^Sqwpmde@7ylgbt+*eFN*9I!CJlB(m%W#^;YEb0E)M9^vWfy&5 z=O{}Kow?%VPa!UZ2B##$fz((twZNYG({;q-$`*x)-Y?MZnE=grsgdD!^h4s_Bp^kZ z<-{z^>6p|GzRh=o-B`>5uHyy!ZE1-*t`&J44F7UOL%rdJ_gXp2K^u8Z)A zMK(86+Kh;Qe|+KZk$&y2cO7q${#xJrgBF#4YD9kDyq>)h?L z*jRC6JW*1EB;S#p29_57Bl(jz0jZ$lNdm+#juFMH&h ztgk!%K^zXgs2+B<9Spqjhv);L{9t~E)e?HlC3oL65yqK6BSs(n;Akj00Qb*>QZ>UX zEit(wk3VS#5b(Q-BZxfZ`1ZDe+=yMd@t$eTqWSRc*GRI#Sb(QH70W2(xAO|Gf75`0 z;LvB}UmAS7zs)0p5Zk4<^HZXJnW8o$H;vG5?$0N4YpG{79P)QL%gf6lc{}N1?h^`Y z+l`S02-V#kBUmQsZr(Rjxsm!{iTwB!6ZD^No|HfODCT8SWCx{w-oYe%<#BfCT(l)>#u#OUWH~}WuTu<2Qma0diAQ@ zWhs7uP$#*=O=&)RzO#yu$O{tm5Vz9Of)QBw*j6_$8qPD7qUQ7)mH9J+`C?kjwYqqX z)nNyD$ea2xaZ&M>HM>W>sJ*y{S7z6qbD0Q?^GE_-okHPdrv*j26O>+OS)+Na`pp>B$*s;YadxBY{+T{}4kZK>;x zM;jYCHH~T}J%|(fQd3fKP_|qO-QfW)mUi4&x zd1^4wTj)Rood9{If6` zeZ_Kzk{!$=*?vYwMn90Ci>vitKXf#7pE8_}vX;I<8ag^UD#elq?i><#%*IUrodcO;$TgTMFyzXr!INq`SI2e8C5lT<{cV_2 zS_C0;8;!YHwq$561@FbAyMi%;@HLbuL^2d*h?f&cCq!MHw1x(=>`4)gN%%9sBlFjf zk1H|mX)r^bYkB#(xw*W&>00A~4i1cqOopS&Ec)(bb#?nPkDeTK41xMEUuVevrGtx@ zdsv%}#FD+lm|zB}B{sI(d`%_dk&l2hGV2C@H|1YS|1Rr8hL3xdwysh0{TqvY_qY)q z9pH`+{{`0{#)y+bcG%@l8OI{U^P%!-Sg+SRuC3N~5naJ9k3DO1OqlxRk(3}Mo9{q~ znX}ezhE97=k8nL1q_ud{($2zaCr#_+BFWA_z72KNM7j@(TONJl_j>s|s|!q6s?8uH z=j_Dt^iO%U7O|m^eV<_ImM0hrk5JD!rY>%*<@e*@ZsU zdSfl?I`Y#NP^qLovnYW19!?XX2jGVK;2GU{^Xv*YjcmLuBdlIfddE*EsIfy5r5s^B|5IQ zYI;9)FS?o|eX7l?guFqn)Dg!IUdj$VYm(uyh?rZIRG<353qXZU-%M3}kdWU;xdW_u z!RS;OOVXRJ!Yp0elI8%C15j!<@R`@8Wsux0b-ruI&andy|Hx$xoK_?A@jLT(-y%2J z1~#ZNUf!F`aq`1dbJdON(827x}f=3HiUe*Q)Zoc+sIo{qs!7lL?8(K zCQS8gG=}@LF-wgRagqjy&_?al#r95wM?;k1<&VIN14&%MUH*3ylwU9}hF3}hUpo*% zSOJb(h+yB6!lUn)0!WE-jwN(W;u?scqa9(_1gL5o=8r#Z2>N1Th#(>vYDmWapzEBX zb8Dj|9ox2T+uX5j+qP}nwr$(qJINQ@cFy0I-J{R-x_;Lfv+AiT;z$et(WFg6u?CqC zKmKr{yx`p=Ui5iYn34tZUZ85;zyUL2zQk5y2%T*&d~dMx+I?WnE=6|-{K0}i4y3a| za}-qB=n*jaN$B@JP_HAn#MyLl0#AeAV>V=S6H4E|=U1z%o|=UysDBLox)DGD%P0?s z&SS29KmQM9ely`t&L;>2G$Hc;NpBPG%{JJ^4p^1ATz-%s+i01aBKwfZqPI) zL0(0bL?zImpp?L%pr6QKkh)zpVL?A3Zi}fI!Btq~TR}ldAhgqe`kmMX{zyuV|1P;6 z3zHqj5=Qzcm{v3ak8LU6zAhxTC%%EP-~ zN(8%eiill=1_6Q`2f_?bHxBvx_8+9U0UY}Af6dNZXpMuCktP@~@3^#%D-#L6l?}gb zQcXY%oc1oi*m6_WiW;R!|4t(!)tlceUamI?yOoMFrZ8DiyB@yx84rz(DVoW?WoZBH z6c7+zR2IsK$q*G%aplx|=JzqE%5~t$$4kksN_nP4FeQ)w zx|NINq^GNIOiJMzi81VZMW%SdT5b-^*w42+ztgL5%#V-qau}y6EiEP*AWcw`-v?-+ z+_`oO=$H89@#GHMu^X7VU*BDRp4P!7jHv48m`~tWbu|4#(4t=S2$!F#wwIF<3SY5V z94(Xon3S#8Qr;;9tgfG!({z=EiGODR=ETL}vVFVQ7bwamX@V6{omv<&pEQIJKNLN^~)k7uFZ(wa z7aC8F6su6m)=U<|(@}J5D#S=vBY+>3(jR%*I?X=H4xcourdP}~frhnJ znjLpgnuPO=$iF)a5^vl99)|=K8ixlzeO*5~BkMkW#LHwh4c{AEBCbROVyaVob1khc ze=Hv#^c2^KS5U|<);q)Uk;=WYfkCFNuOAWm$3OFuJ3se54!#8{d4Tf+*u+qg(xs;} zDUr3MTsRQsv0FQzkS0p^p)D6V!iuwDXy;`Rx~Nk=hSwNQo}hE2mu|Q1%lGH9V{!A# zv8#W5UD94D-Zf2dXv$3j(ySrsN4P<1UVeHe9lj6XrLwfZe5pjiG!DSMw!!rkXUgXA zWnAV%hMsHJxlHXtLBLF3J0nR7XeL$0czNwU!b*8?-JB2|UHToT!Qv>l8)hi2H&&db zw%Mn`!A&KvygQ8UdFwu+^0=pX<6>^;4$EPB$|F1xr|vMUgblx9@7rH;-raeZp1s(Q zUfYG3lERciB_F477sCA~li*lH5I1pD@CVvO!X@a&Xp9obKK{%V@C(Q(Tm)$D} z9tO&hE&_949x$;yf85~0Q!Q@|Kta$ZD8G-c#_Q@mG9a;1CU^u2o$llmL{Rqx6DQ^M zP#N`C=G6?#{(O+~=VC_xIxcT)0P1tpOVJMgFs7_QW)}@v>B7chOIIJk89>!a2w0P$ zK88+qp5K2J*c~msk3YG3eCFl9l>8=Lz9Tuhd3U?Odl4%4Iv&Q#W`8}4!cZF47$%;c z1M#0)v`1DM55r`9d)d;c>(>Fnq1y+*iyBVGLX{YMe0s~3T}o3B`(Txdz%UB~`w#w+ zHu|+GqdK?;<{n_8u?6$J8yWe2Ir9|y@86V*bh_k(1ZUOUX#~T%IL~2eVsqtK%```s zOX<7bR-ge#&aV`ibP@pSN%QipbcF8_L4Q>@vD1O+6Cl0P&vq;1M*-`oJF#y%pTTI* zPjq6{yF=f{qg^%n%~J_B3rySTt~zN>q|V`)5)@RV(s@93^hqMN><(licy04J?|QHL zCUY}2C6(?#yXyf7qzc5P4jDL}+caS@=ujHMbn#DWtnb#vbc9248K>4l+StTIhBiBE zF|UOwP)mO7LRSA0Yy|c%<~zGwwRt4kXOeA*B+J;;ziOH88Cjmcb|*AIneWYtj~;cD zRB@-I8Ea>pK!fY54FjO{do0P*Hmg^fcm`{mR6+_4Yp8F=Mn*wV!u#Z|b&5|UNB3-F z-kCgz0NvTFqYo~Ja?Ls&n8iXDTad&kWx17`R!&-4+DqSVijUsLR&sqExe5Tcc}p{!P{QwQSqez z>9k~;A_VJ20r9?K;6#(UQCu_kBVfjOs;Ph|S`=%`djg;i!s^RfMnQ1Twrjm@TCmXR z)^~)F@N^>*) z2jI!G116pI^T*$gYdj*1Q<;(=5OS1g^JK~J3!c2~(WkH8CY|>A9O@xhhN7v)jSv?n zQ2#mG2!#+B31b#Wh9kaVX~p2{0~;Gi^FR4}!62}(U!r`Z5nMU zDP#PM+HzNZA=N)wWqM+1=p#9(A!K?W8$)2O5vFd4BB#4SK$8@Zj^`QArQoLIB3W=>p#V^|kzu(~EM39FMa>cG5t`F}<} zg>+Va_4115?GvNO^8k6&@FD@+(1sw)4ebBv6XN55T_oK=A#6_HclgfneP6eQ>zfy` zo6r8cpaMa`SPQqgv+UM)KCg}@X(3)j$zNUKUWNsT;^p6veB{b5#BtW@sN5&#gs4>z zI`GImOi5Y=AeH?$>8F8_V)$-6kHSae-z>3OD2q7px}#CV-NSw3cs0U3BVYL*hxu_J zwwx-a2QFRfzpfxyX-ykpFIrZqoAgj-V~3S>V&^Nq=(Y0+7zaA=s)M#~RA{PQT3Sq! zwuvp9T3S7dADL6|Wui=V72m#naW@kQgaf`+o$qi&7((@MyS+hIdRKEvF+PktEX=GK zAf2sMm)M{}Q4JV)Fu&SR(j124f1yp-niRGQX%#$ zX4S?p_9dtB6rO>ApXZzB`w9Z!jve&$&k)2Q8W>j*U0kw9d_;!9O`ts+|5woGt4d0! z6m9a2!;3~ISHnWhf%ZL+01qb7)sQ+XP)Kr86%3rpk^G)GkxChTLQ)Xu6Gy=Y&`v^- zpHZpTG3T5Jz0pHDzVbh5*V-QM|HA(m@1n#Mi5VLE+mZ{i{mS|JY`#%88;yOUhcCRp zgjRmOznJ=KqLF1`YVPn6CHix~%56uO1YdoXxtIIUakI&nBib4Kb)ai|0Ai5H+OE%L zcyjc)ui2>ydkHAS7qXsOQf{;`Sw+Sse9GZ^3JbJKityAhzs0k zDqp3XNVG{XbJ<-T5Unm$SRY35hx_N z0iC;rJxSaFp;w)dfB;OGI0hyievL&$7`@Q3chCVE-cnrL{C?92i-xgnwM4j+C`eds zSSpE^U*tGwlM4tD>-#(|P_aNUF!ZIa|>U2^iFuG=&*^C4}I z&P#F6f-7+U?{Zicjs){Za$Vj@nau4<;r85YuEUX`5=sUkg*?UgPq>XtL)W`20_g&er;^5_};M`PdEjXz(rz zcKjZDCv!3C0=%U6ylvBnAL-)e%v_E{sipWhY`#(s@0k{VYpStv!bw=J?n%{*G->pX z4$dCGN7N=(Zgq*r1F;;s$Rst9@z&5p%Q}ghO>dc#J^8&lvtae$P=eMXeBdOYMboE} zy{>YE5lS`uFfvmjxUK5=XGYJ#&mETtsC0w^|HL3xS zJ7C!DEXxhoIrUN1ATt_??YHtxsF)lfkSi`g`o*KbQcJ8OAd-M*@{y(Z8yrGPs|Ot) zbT2N}k2U^oqOn|+J%oEoPWPCu{qj9v*92I!toZ6l;IfP*#!NN$d$0Z6eRpEKFBtqJx|3*tOfmZjUYfE3{@BS+IZk$kb9sl*6LJpU6R`j-DhbkJ8q0WTpKM5GI40d3f z2#y>!o;P|ed^U5q{CBKl8y>uFP`XeE6szqJ2*qKfr$Y?6BWKE|RtGOT=eb-IUzwRD zb-Po}{c@6{BL#M5*{s+3T+o_2IW6ITBxP{afC$KK9xG<`#X%DNAUC{84nXIRO{SFH z-8?5~>Z=S*2ZQ}lS$cYD`xr^rXW^vNcP%`kMgHcdvlB!NS&NGvr``TFRj*b-NCa(3 ztdaMebr8~Mz;BmW=@>G5LBqT&vfrSZiWlWpYXW?r9#vwV1Twfm6=`=@F5!qfU`NM?jTcc%V9E(*2 z>#YZ~L`Y0{amUl-(bb=MkfD^Hz>U%`^@TDAwN1M^u`sVS?wSBp=f-NO{J{+e5Oc97 zw|w1TGrPUNyCRNL>o73W+OOhx>qi;~OLwcM4TdjI_XbEa0O6+OhWlx)GV>=cc7@X` ze0@Nf)1!*N5>Rf@sN}6LlAfa=Bu(Q%ZvrSXad1Gm{@b;EZ?HWtMAG$UF*@BB?wH~m zlcScI%mp>0U%zl<nSi z4YPLl$~zTZX7q-(@w1k_a$Ie7Dp=EBsrpEdr89JY!P872wY zscXw(FD-7jYAPLRH`Gm3lo`MnqKIZ$5`64m>n?XI<~k1MgNR1pO2e>C{ZwL}hl&Tx~&Jw^^dbq9dF40;`~6ky~wQmt^5| z-GVZILYV}cx75bhBHFWl6_QAQo+&bhCfYmrBo=B6(K|LPpIb8wtw>xWR)Y{6mI?g) z#30}A7FJum+m&zPv75#fiR!#<7R!)zwK3C2-jU8%xuGlg8X3 zWbCElbT@$@LV?<#g@*mYRCgkw1EUu}RFEMfZ7?Ul9Y>9IR4=`E{6N{GMbHQ6aI8PRj1p2 zjP=@sdvLcdnACny(}HS~%ovkfE8qO?&%#p3^vBg34O?dEpi$nP5##XZ8#nBY-F-DLIK zXDh{abZQ!w1@4}xr83U?xJNU1An8{4yVPA<JW>fx!>4TEX5^duIF3X*M;*-(5vrOf3o z(?sg4FHyIrS3F+)Hrq$xl^s`3$6`t1*uXZgHAMJ5AA}iu9mIgig5dW6;ASKl^6H0| zgLwvqu23RjX&W-t_Dtz&711%3cBG0|9#VdtQ068vqM46$P%EiSMxZ@E!X(b9S-5>H zufIYgZfrGG{XVf|Akg08ra5_kJfdDi7PAgx6aqOw`4K_Ia8 z#f$4m%`urJ`RIuqVl;(6N{rZ;fl_OTl;d0z)cr;<2E~cu@-dw$Uh7Gy4)8I3;jB#I z$G8#jLXDvXIVIQol0J;)QEyCoYBVG(YyW(Qpgzd#>X+OiqnamOHC@}dX!7K(Y`w8) z*^M+v(S;keO(*_2+=Pg80lo&COayCZ3>z;acabHI;`gWNi-j06=&j7@FHgVwdG$k2 z`IK}vONntmid;PK^SW|Zv)DZ4BL9T9(&&c+eYg{0&XAB_I{<@&(*qhIJtxQR~%+LC{6hKsS1$G zP~_=!%*aPpNP34M+Ha<(0v#n2LDDCGJD^>SP^_}k?*mV#!wLGdU|_QjcWF2ykM{0b zwfn?dxd)kswF45ri0M1z@XET_ymW|mynp)I%3RIH2i)`IrfjvLU?tjPxra`;!RNC> zxY~8P<}P;=EXG?hdlOc^%SE~}Qdzt@-pza?41{Kjfo`^;)vb=E({MTD%1Z`m7a=r< zJ1ZvV2UI21h`mMX_UkJ+3h>b;bPu;Gv=24o%G7ovI=df4CC)%Y0Fi`Id-lfXouDed zVL`jidHcO@PWt=RuvuUJ1Magam!G>8Urv(i54w|P=6qG+=RcFPm!OFjSKz)*u4R`JK2u$8>ccn9lMq4rSVfn@gX%=oXq-oBpRnqCjuutd4^D;{T0 zVSe2Jl-DkOQHYt=>=g(!q+moCMj9Pixx{Q;Q{GDcJf}nbLJVJS zvg^ourC~U!lCg+6usvCq*HnzhSLe$ps+3yQQwXkzjQ#6|JMn>~2kVs}ubL>?pF2Sd z^CSQ1=gPKw=$x~4cJ~%8{cC9nY#c1dfAkDsrRnb{$M#lc;s4B~!$5^=KlTU>YJ)&s zs&DDb!&u%T#x_pp8h*=>l#ChvQ>>mgc`{7bT>(Qc)}~wzun5%?rN@tK_z=Qg{+^3h z4GsAEK+TkaD5xkGL7L;`Ba@&Fq;HsdRgX`9CzB$Mg^~+y^q^Yhr_fWd`(}hM$czPWFgblnR|WMx14F9|Aqz z33fSr->+dW4RlmgmYg^FRH}1WBPua6(?^RDp_g(NkC0Bq&cvM&;-Ia9Lx&PC*l#d7 z@8>4X>pO4BfAOp4`CHL<7s()%A4}_(>C&4y!aS)D(q%aK)Mz)fX{`#A8XB^cd4Z8)72@+w3Q#L zFpihlG@~q$Dj=cJf3pA#{8=(p;DUat$y9w)f5-8pp`?=@g`iyB^EC3Nc{=1 zuY~oI#)yn8frE4@4Rv>KmLW1Ub~T%yCyxTqL4ik6$$Ra%etSk~wQ(7X zry8nv4E(`#-TZ3R>}bIdBar&jIXgQ&Jw3Q4t;H7EJP_S{eNu zCxOQ^7c40*SaxSfXqd?k*o2WV93H+X^rS^ScXGorT;m@!w#qU+s&%lH9skczHVfU` zeAgEoL@HGK?T@rM_2M+X>NeNP4#eA*3dAtULsoT5JNQ; z^CkmnM;jh)sJ+m*y#A%B@|;VxEWsXl69C+?KQfNqcb-T`)lVX6(55cB~L7-eriUf`-ttHXv?!opALC}f;(VA!CS!_<>HCJjoOs_45PC?an^ zH$U9lWCct4=*N)u zn!n>bG7nqc)W6ecW3p*N)gg{*EPaY74S`qG-*EloYAKd@82friHs^&Xq+U)VH>wtX zrgjDbQ+wG~@1$o;=#RxsBRmW>k-tkNNJc*Ls%!Ck-Kxv`_pG6GDYH1iyNt6r+zDhp zo-Gk<2w^>8=gHE-hMttH}C|iJsr8B z7TQUq-7hyor z<-C?cvXV1lKC}p>qCf@KZ-WaCRgFTqW%=^-cm4< zi=0BQl8ho+S4JapOfXR!jbR00rB*#ZxvDO_?pSbTm_%V^$1BZ)BX;1G+vP5c*T%gx zrQG!eSeuqnUaqcaqbgDxVT|nIvPf*Q|MMC1!>T}4Imf~}^T`eymY-whdj^)4mXtFC)3)eP&k0B(ec1ff&Q}zpdt){n8^&E{So9} zXd6oL1#(G0&2I^xleHX#(++KfLL07x$)FOI7iZ^cD73siz4hLceHHOv@kWRN!{;OF z*7BrVlkZzJHq@%{$H&K_N1uTh3Ki|q#sX1u4Z;X|FjpQpK${fE?*;~vYuT)VFaRIH zuEnNB8zN38>&uTZiO)#{nJ%Ajm>cN{hDU!k)`I;~JS-w&WSI1r=gtqSLXldxN3N}k z$?DZtPyPLSAT-4o%rnXgq1zWmQ^^tqIP3KpCr62Op~4Hn-ag`;rUfzPlDPVpb+QT4 zz2Gxhn>9ULkIbc(zNZ^VTK>((M@8$>@40u_=VZNAO`mcn%WeX|7?Ahb*)zj0{ z3D-`a!O<75bmo*h%b8CC8Ny?Dnypa{X9+h@87(f4tFz~ME9I)Y63g3&7;c>vr~~#? zz;8ypN60Oyclv)tad)EGWtu}um@s^~0BG2WA}tPifSq`lIH4{E2Gplbw&$jG94<5% zSh#`er4_BRC&}Grc6adr+UpD63=%%7019!imISW&gkQm=6>iX$Nm}?b@$QRW*343J zrUt9oD@()>;W9V7lhQ@-H%buLj+^n+3?7V*6$X4ZnrteGRbT@b0H3Y5;*;$|F0t<@OgxF5q_pbYSbC`!%UkfI#w$6Qf0( z2q~~i^;ZUMDQM&axfDS7<6OOEg^0A`a1A`NAFUo&dOtA72gnm9IGy_#n4CJxKV!=n#UWB%CfBOZF2l4I_7hVmLQlvvi^EXl%wHG7)i zWWP11B1w_$RbdJ@i*wcD-h5=mZ+1boG@SG>6^F4jno-KIFAm{$v#EXGBf!G zF-I;+quy4BNU5&kqe4^@k?%S6amge&{mZHhq|&fID1JS&(^E-2TM;G~crZ5$>}o<* z;f7D2=f>#cwsjbpmege9+|r{1qic7~Z~zq@2j258<=8h|6o@XC>CV#Tkw)zw(n`(S z$dwmJ1Rjcr5vl9fbP`(&+~C$}={M9%j=fsrhy2jkvUdoQk=MP4<2-|l{sQB8&U^<= z``$3zAkd6EhwW&gGBU&*arC#xsf38np$P|Z-*bZEfD-jbxgRHdGQed_VduW0%rZ{g z5Ce1(7ducWd0z8YnLUnl(Sm}*WGBW@H!>#qCYYgib@(#nVAIAXv5aP$IB-%f;EW2@ z*I#bZDbX?gd)&tuCI~4(iCTFNbpH^Z?8jUyS1>;4s8ld-*Sx)!>uVVic`W=_G6H3d zZ`P447)~97ub#}Jbhg`|@5DpSo!&3%>LHG}nhPA?ehN9}*rsTC|lpZjfv?gt# z4nW>mPp{hr-AsTn5m{osL{kk6szSQwVWxb9CYveZ2_gQNO4)IkLU$=ov24tz&mP>b zn9<;!{r<)-m*8?Lt*guNqRwZv$x^R(ad$Xr+53FrsNCK1V@<&Gbo*-|&1J`z)x}UD zKsVN%^76IynHs={>*k=B%PlA`jfMo2=t?#w$>@Mf$A?G1CNpU*x|;r^_8(Il%ANI~ zKx-nrsYJchuPJ1kdH_{ARL3zaHwkf#Ir(&(=C4dh<_V7`yXv)yTXEB?G^Q;$h)Z z!9nz9)o|2cb8&f1N$oR7h2Lrl_0hC*H8|Ka9p%J87Pyesrf3tnDvr0!ETq987ALM0}aY1Y64%Wn5LoKOM)2GbA^sQ$H}>Q9;7Ug(*V2 zWYW|?BwJzCfgmCHxG5lh?~=QNLB^VYJJLh>@4+cPFt~rO9Tt~ozEmj@HM@wFX{jFc2{ zBd4si>~>qY-@OKUCu4IIv1xFSCM3HmI09sA-*J{oI@!b}br|G4@;#3bMjSb}|Jx z_;JW`faA<54e!)_@?=Yri#1HIfI|N13dh|*w{Pc>eX#P z^an956&4qBd>?B*JfOCW{>`dvr$#W@b)1&|r73-JR#9 z9426FIlM0_hBxlTEwn{}U2Jx311`+5Gh?U}xSKw5`;*7+xL=4=v>J~AzJ-%>p(XpByujjOOt zI`yZ)P|;aaWeIa19(^ygsh331L~s@_5(10}WP60T+?8zTZ8?aD^pkiZj-XNp(F_l7 zQ6~g2urrUoi*6}-*2$?M^;2(S>3H{#p~S*}nc2JB?G|6LAQdlgi0#iYi1SwpUQ3@c zo|;rd!SrktuzVRgPGD~|3x_`4Mm97O$5{?_mB(RR?%cy!S00|JQKTlIC(L4Fx5+R< z90#4*3x>6}&sH5#4XjwGqm=QfY`0G$bKOVs{twDQzi*Ess|ea#M>>+1-DQB+T2S&N z7&b(C5~}$J1I_zi_oCyzUf=vmCGwTT)9qt>J=0omTO)R#5e2M*&5WAwsj({@Loj~+ zn8qMte;_qtR3sbzk4Y4KWa(q;v{XY}iOCyT&FGanvZO*GU`YC= zt<9T0QLSY2`WncH@jpnyacoM#{AHce=7Dnbx206~=t6j}21+SUG0UQeb6*u~ja-L{ z9P}F*Dxo0Fb5D!HBhco+!BY#W%-M<&vptr@nwORakb6x_X!Ahiq2o;)Wm|L@ICKzu z6wp1|2u_LDw`>ob)K666)`6LK-epU15s53VSj>m7xghewA&DwJlhPrnGO{h%AgyNL zO&qsR#+miOkehS}P)L&A?PmJ2#2yzLP*ID$R1lRkO(6@Ek=-f_A^m1xSMcbr1~S)E zO20B{XhX^cK!kK1JLm4()&M;Mpn8mS=R>Deckn$45?9Dz)zP)~xo?uR+m_GVBv>KH z?r#Uo^WW%w`f~9SiIGCKW%J`ffZeG?m3id0t{Wf!n@KeJqo01RGHL)7S^$=3h?^yU zKKT}*xV^FsH%snt-ua^^YOoA|k2Jb}C6Zc3wOk11$rk!whwAsw0i~O&I*b7jEU0Lb z|1|{`ftRN5^+x4n*7UoklvJ7)B8sfW%ttXUlUc&cD*x1f+Qmc?NB(881LyBCoPmf| z%!`2LXY0o3=@7NXjc9s;#S38tgP4em%cE(*Oi5bBz3q8Pe1-cy{qiKIjIipW#`IuH zHI;Zo-u&kyWnZVY%#F`6`0c~vBQTz9?9LIo3!Rg^z&6SVYfX_$ap+i;a!O?Az^CX> zsU-gp9gVd0NvbCTKKft>tqwvCFwCz^#=X;1Kp($z7xgq;p90{- z#8}VA2i_l)D<3g%aPTM;NeM~jSaMYK!jgm{5HSB?wB9*o(hBR0VRjAN{kr=mr|!m> zSEH469v4izWFvSYn@!#u%3C`RipRSCt>-SgGE|HRX)V42utXq&x*cncQ7JI{_N@&76y73?00wJTgyK!V1XMc8X z+pq&x+*O^0*6uv5_|p3C6X6P6J@@ZN5%Qs{kR8_~iHUE6my-07m*TLyf2X2ca4~oE zyx>+L0kO~8BfOfn-O@e5f55Mi{WqAb(Opjz!~`qCOjcayqd%w65kvxviGc~s)WwgV z7>zK&m_i$Cql_6W-H@2}kbdbIz+nUj8>$4j(ks7aL?Jzeg~9xv2d_R78p9iC(UJQ+ z?-!A`SAf6*cmFKUof=DE5k=|K6C_LxI9V z{TJ#d-^f4;Rq1XgQ7vA!fE+e#Y9-HxT8<=f!Uy8z1;qMiM5hx-7=JCCGi($mpdWiQ zH$gg7Cs7Zi|2nQq>F9t(kwjgJPWA4Wi}I#`$A?+CbUfb{L~;YeNJx$-3vHr}k(ear zDI0bIpjUqx36%}N1o2)C%VD|GERSA*jnd0*10dy;Z*SjC$H&KUu96dRh?be#2?u+) zu$skz|KR5zG&^7n=^X_6o*J9s7X>>(FTzBTU^+mmQxZju{s#y-o~tb!7T@Uj$UD*x zWAUD~9#a2d5P^?&rP)V0j!PWq$*5=HX@aQ}QA4(#SYI<|V5m2mZ6knF zE^-l?rgoqn6et)BDw&}PcIeuY{roR6gFU{k8;S~j*S>F*#(&Og>`ayhMKuMYo&e&< z7Ssu5#FDV^P52;^hBoI7jE>oG*+z6MhS4zq-95AGQ9x|K@8AK0=+9oe>X!>c#D>uC z^Y_xT6*!e633(y)bu4#rc&(@u;r%+)?_niplq1{tnD+~{iKdtAQ~4aMxk^9P$E+)My&sdhkLmx2W2F{ zsAZ8F<2%)XeSARrD_xYs1SAdo+dhB|Y{GKS2aV+G#68OoF{cIurLCxF^@Sni+oV9z zAMn$r>7ZisWt(eLre?_U;b%~O^DN%m&8qmiZHiLu+~*{q{iA0@W(zvZ!3%)!7|SBw z^L+;^RJscTo8eti=dTTbl=ZDby3?=4tn2?}h2>b;O4u`in6wpybfGmO6bRHZZobGq z`<>SaqE?Jf-(NCDj>tz0jf4CoiE~dMXdTVkkun$sv)&cXe?Q zb;0(ZBf&(|2_9*!bozZnNb{e+yoLjI`&9i0J*ar=q2no~;yQpa&`iNj-(nrL(t`TM zs-H&sOQ|p~2%ID+zK8|f5VNZBkzH?a!O>7E9B~$!kmCie-^gMN2dy2TD8v=|2=wNt zsj1nAF+8fH%Y%+kV5LkfxuM3yqA-=Cyd%xR;BwaE^~?lPco{X=y3@b-w$#zdk!+V#T4rEdOOBStqmaAkR10I^YGkncYBPCN&Ij z&ROdqL2tZHMGq+ppf>D@9^due4wFR~VgEQ73D7YMVX|hbEx%tl&-H}dB(KlA46Zdm zz_`={<&6>c-Y)9`0}{Ke+C*&PNX>Fhw2Gb2;j)h_XJpB*|1hav2x~6J?+KrN@XsWF z?do8ye#{fJ7yO3<`{4msl*{F1GUMLRYs5bp5PH-(Zy(5U?i*lqVS?M=k$`5ShUZni z-p}M|^I^SJvTR-t0#ykRWhPrJ#buSIs|TKbfFC>y=8TMt^iD0v84SzX?HI&K^HScI z^SUnofzb?79}iHxV*-B?Ylew7M?W!`(ziTn*^5vt4Fy$O_}agZh>K*MUQjjmyH7JmXLqzxR3cO`wDhh>Mj3D zsUrTjtfegNdR=x1Qj}RUBAJ;&y0!N5eFtt6;CYlCKBZ>_N7PtxY0Lk2|1W(C8<8{tC zXF@0RgSs;UU<7nW|5x=#F~iBdzZM_0Wou5Vc=_=077^l3;=`3J?$-|zUCK(IXGlsC zaqc_R9`M!ydmJ<*ds)vy43sUu`W>eewiYe+u{BnqCh=?BiEZkbS}X^MK#`TW8r0_j<9lRMc-y?J7PYO zw*Wr?Iz}-4WOLd1Srjqr&pXF|r4#k~$ET{=_-g*YRFG;p>K*f8Z2f0>xBCXci^ep6 zZ6x|&4o2_CGL(>pt~54x`$yYnB{Ur|$T&+wIyIX>O<15z;>fV0J8f zRvk~`dU=N##MZSn2a8yU1@@%x34vVI0l%fWTe%CPcB9F7g&~|w>w|t#-aNwl=-)hq zC2ZZH8aXrD&T9n;1k&Dm<@4Oz&56wWGFjiz@s!AMhPhxAh~p;a_IVyQHGa&(pxci|n~A9Q_kj-rhl%SWLsBWn zz!0u?a4u~(8aVJidwg-W_2=hGEkCK-U&CyVhj^aYgk&gRQnj8Bq30*buX_bRO+#P4 z7wmRRS+-z4n+ZAzZ4FG$#grD?hO3LhKz%MQB|y5V;mW51t?*A_Q!>hZ*t2|(2WtP} z`6v(-i4Wuy^hD-aT9HM6Db&daOhjTDrzJE|T2OG_dyFNrt0{mBaiADtdVfBMwN-tn zP+g>Z8U2-i{SM%ONrl3khe0WfZ#^-RkE6wxzu??dKRGjT6QD<%vm7}KX-Tag}5ua53h1l3Q3My7)bLuc-3vjg9&pX z5GHMQ>f%rR#hRrV0)3RZ6^>g@FKDm{HR(2>c%4Y#F}m)D<@*lBlx|qW_aEZ? zjlf&yP{CbWGYPX~sZ$TL3o6Ofw_imxp5h^wsMDGXjZl<5P8^%Vc6>+2Alcd_GL7cn52*@#tectI z7i(d`3cB`rq^H1HwbJ6!QMw2EdvQUO(2Hf?oQ=7#q#OQGaYL#e2 zW(y5APU??ze?$VWWg6M|HUy`2WL_Bw2qfPrcrpd*v*HC@Ud`zOzrPn_6Fw<1oA-dm zD?mSfUrL=@9Y`Ei@E2+VPZ_>Eq>*uQO@NDYEVFp~C9Z3Z_1K-8nUP+-A-uZ@601u+ zn1!vi*I697iFE8!0vJ(st!jYAEu07Bp#qBs2~xYB!TQg#&X`Zy9VdXBK!?9QnVFwU zhOI>>Mug+W66;ryPZXm~`^|ZAP+T^1vDa%W%No9lN;3s|US>t6AkjOL0tygz9d}+g}Jn?u#5z zbCGpkycM;v$2pd%5)PhRABc~9z}a+kcc{$=O-QIaA$CM?cNJ7MG%Cbq7q0<8|Mz0g zC9b~%76=fK{(qx9|7S@|N7TXI)y&@2K-J69%th~iTN0bg{Ad4H23CQrkki zZ1v1a>%TPwB;{3TWgoRjRoP|5bpQn#k!SomzWmNxlXwRg@ zerYj1^Cvb*cg~Xh-d-n6h23hIvKWOWBfaA`{`^m6XCBnV83phaMTs186r`dEq$MCy zj)cQdu^c5qj)o$(rHWFG+ys&Ut%}MKq@&d7C?3VJ+EyLpkct*6!w6XEs8Fpq4T3_c zAdCk`Ia(O1ec848?e3R4WCobc@criZj$~i{$?KD+*ZsP265l!V&d|2twvQJyWe;^uC@R3tSBftn$wiIE9uDN_ON3uI(B6@+K(n}`z@v?Cg6w29Q~|>zl2GC=acR4 zZs(`-l-!;NC7^Nl@Re4oCyz!JMTKs7x77HtVqs!lW1ZxBS5n-uiC#fndST=A6N4S+ zilR?cEC_Ggmoo88*eV(r+mRo>G-zNfJwLpktDF+}pWb64%(gz!mMjb^t(kuF?8>;7 z-PynBm0g;5aOf$lshg$a3vEOYdPH8~varkJ&WZQoN&+pW4}a;84RUF+!iC%)$UHbT zE4bzg{I?VAAT*(!vVmtd%Qi{mvNVOuCYfBS=~3o!A4|^mVmQ_hKlF7|4q)=)cx+l4 z?(~ml!jMV$ziund*HWO5XYj0Msa2X>s(@EXtTI8CiX(ib`jD^((XF}!u@nUmzH=!8 z6+4rI&BdJSQH9?FB#@Ph3-ey;kO{%L6r1fItbpS@*6~t3>Ud^?G)wDo^_=QMr{TC2 z9Jio_a)d)2-y};(fn@k)DC1>va`_;$9A+(tRXf3T;5~w{r7;TUQw&9N!e(h$tU@W3 zD_oEZH(W#}e>?SXK&Kgy%W1f1Qw)wuH4}LXhxBh@zNBj3;7%C zHgYzqpnNt#Y=L&ChibA2aSM(8OqQX19x8Q##dZ7F4pc*ko)9iQRAXxl4ha>3Vnh8% zBuIY%?Ysngs>%uKY|34>#al2EMZKLrx;Ef8%r zV3uADJ@$X4_n4XJv?~V35+$KnQTO>qqj}JyKf~K7y+;k*F=Qm8o--4@FTmhfq9hb6 zx;`#n{1%AD!BSSb=$HqFjEH*8OflKnP}@u z42~sALb0MtIfh#%LA0JF+U1KOBcdy2Cb}#DgJX%3P^{?Nyw;#cPfxOZdH4ik z$cU($>#Izi5gH5=A`FjZN>Q<@^^^X)7DG<2vQ+nqF=$lvl{Y84>U0Q($5th&Sk)_K zBB2_xI>;)&Q5c0zl_{l)|0V|H0&BXCCy-Y`GP$hVsJ8@zqfD7ltf=;hAt-_(BNGGm z&Z+A3&rDnfb0U<+oL2CIA0SzLJ}Y3u+t0n(?h2u$LUC4?5av(28Y+-a|L}g=dWMv>eJ_>Vma%Ev{3U~q4ooln4H*%Q2&#xG}lJGFa zJv;AhE?bgPu^d;DvMWx+s{KI8uETQ?={h8}B&F4sWB*2fi2qNXZUBwT3%v85LrJ^4 ztks#p0F6ciX!H$@-);Z*_IKMI|GI9s+a9Og_WkYlx7)YdUw`?b+g^XzcKYwb^{mp} zc6WI^bi<+F^Xb%g)Bd>M@M(Y8O~>PwCPUXao?o+a@Rrm1C`!v569{9H0*l5+}oFp`*OVO#)JHFIG!#~ zy7j3`r~7#jfO|$BqrNG4$^lX7{HpP(*UKnM;6!w zdH4PC@&vVg!F?<^45!Pk+n*qkYZ3}rvabp#vfGz90MkFZEnWGvVVh6GQAv6jPqHX3 zyWzMSwmmC-+OZH|D0f?!gOo4S+qinC<7I!?cMBCAKp2g%nSK=Jv+GX#-7$VLtPZ=& z;m{o-R^3FwVJwhZsI~!j!)c+Oj8VonjhFDIq1K~-3S?NQCXlShRy|KG>5-}4%TycX z>tk$HE9u^<=SX&L5wgB)RfaV;TZ9zU79sRwSs#vv%U)Je&k}YksdFnyFs&XdtLXfQ3+kM zQEdc8wpB8!1Jk@>RNITc%SN^NWOfl}Y7>}_;416$;CAWiVFfwjcZ^Tj&6ZPT<*@#1 zHmsC?Z63u1G*a_I1N+fSYw z-fxAMhq>hxtu&6c6M%>A@_0lX0QWWoP9M1EZqHfj4ha7>?S9kh-WpqNA+CpTw)+jE zmtIFs38bF`=hLZ| z@5$CjFfD{-v`IDja+X4}K0sm$^T=p_I{_i*6$`cM+3Gt)0ECtbwA z9dz9#Q(|SVFIiB_5(I+ekyW9hw`=l?kV%^bnM zw=XsqfBjTs)a}KU{2jIzAD?c2wPOib!Bz|S<+o3_-BGH%e~JhkE4+0THW%OC3k-gF z9h&%`win+5<(uvE|LKm3FfWK)O!(pC51JjTb_ZV98`iy`?Eg5F2Z!>-?_Ym>wf*ku z?akE(fQHj20Xc3Fp*%>@ssxb!*<^rTY%ji4JVE%Cet-(M;RED=43ro>j1FJDdb)AH zS#A4})~<~gsk{rg>2Sx*^J|Y@_)}u}>f^`ne)H>JyCE|N+l$NZ{%~{k@#^xOeq%Z) z`}XN3b|_1!XT^=f0Y$I7+(F6yfSS|6)Q0`#FznDBizAcick7Eu-0yUKvC@+Dg`7N` zj>mDkN6$M>s2yxrQ9DkjK9=3r%g|SM`$NC6wmm?mNi3z zd)aBB9!l-3O!ZoHC%B65k&^y$5&{^iz5wWcL_l(N1o<$?ScJ4Y(07E?U$Xmzw3Fy* z^koGOA=FSSI{C2xI@GEa1sYgip2nMqP@uZ#p!3pC(Lp0-eq!OVGCLE73APW@jOn4{ zJ6{-w|Dq~rk2K%+gZ$DTdbh&h&e${{ODHf{&?S`h2ZWyqtrmd3jD0^V6M-CJ??!6* zo(5>83pgOMwGz}QkM_pOP~O5&lCprK6~Q4xPcjsbh$x`eTolmkO8^r7COCPxqcpDC0X}<6`Su5zDwJLWU3~gmo{e=9w2vbPt=w z022$QqewM}$n`MhpvE1?{-mH}F^Pf4mqkt}W&C)6r(1{Jk)Czic&tK!@TM@HP^O#O zk1;J9!El7RTUlTfp}$QmaEJ=>fz5^SK!$~)KnCAeV8w8cfTfXG8Q-k5$@rK~Z+z1e zt!8{P#2mBlWh;!>eEJb((J?->>C>@yypHomzVM4ME!YL88DBjAIw1HS$FApuw-N(p zobRw14?5N%a=yd1UM6E@3s_fRg>`R#gf<>6D-qk9$J@%p_BJmwB&{A+vxDo3VHzf4 zthV;f_U3`f7lUFSlG-r8&8yg0nV&Zi^d}50Vs}PBy&Na(%?yaM<%sEJyzB=QuY&n8 zjWfZ72~7l(VYkQLjL>%+LHIZgFxzg&ao_RL%EV&r$~IXU`oIn|vz1}S#^yOm31h=z zD`sq8f^Fr5d6iWj$~H%YV$ksRs0`1jAZkUGBbeC@G zCi3PkjhF>!DxdT_Hpjq{6%+XZQVxUe%=`<&7G4DTxqv6 zIoYJv+{q^%mV607!rxdIa57KUG~c3Xm)h&N{m_tWcgX6Zr%IHANLFbuiK z(v8CSowO3PF9<~>M@P9=s#4sspw3dtBD8Gv$DCK;w$u)i~UV4jT z=@C442N7%v8ueF06liJ#+K3cr5?qgDnZSAe+z+KL$K{lDf}H53$P+?Z5i2Ixi>N?> zG(j8xH?NA{%d@tY_sr9Ytm!!PST1L67^tdcK)kxT8c_Tbvb1S^-W|n9!NYJOF$lLkfc&Z@$2& zCnbBt^EjY5zd6`M#I_Db*s)MHldms+lgR5r9=gP)qsC{9f+Kos=JB$E#M$7QBn3D= zBNTXU%hnrw-W5NWaCq+pV>$-(9C0GnJzmbEBL3!OkC(8byDd)Ea~>}TaM9zX?smay z*+peYwW@jC4X0K;hr7~Y*+peNwyKp>Xw`F=X^+b;D(kUTtsG;kp2MtIge)Ihb8b~4 ztA|Ah-$4i8dM|->~jZ=?C@R@Cb zM}sLTJQ@iE)ZYhsylRcjMYPJ}xXUa|V8dc5}d7DG6d>WV7BY1kAZ5Zdiw7$@=1;CVRW=`224jX~7? z{lSI75VEA=8Dw>T>9@hEqnp9kv9@FwRM=jk3uK#5>wEyspP#B&MDT zBOLvd<58F>_1HDE9_pcZwx!;1+IJgbbda>MIcPr>huPSXPN86e`#n9Qk-0cgKu^@XzCk zrI8R&T68S-oPZU85Rm4R7^G7Ime>mcC%h$?1~URukIgBfbV|SqL?NKpW^hdkJ3M;l zEf7`xbbr)l;8exUXS?ZY+}-xw?rOL~pE~Z2H;3I7t`2bk+`W7$I#x|z)(!~Kg$qk$ zQ`$k{Nx1EAb}u_(P|!UIfZQP=qNYWwf>EK~Let09b7`iV6o#JGGSq1anEy)iQg#b^9}vrH#HY+)O?wt->Pks=4y zh%*69oStIK-*-wIwwBd0(xlLgGN9=j8)3#xZ0tuXgK>x$S+X8tH3Qb5$tXe4M7`U_$#y8n%%26YGW6330+WlO+QY zBfv-=;>N@Qv6kc1I7$vcOo}dsC1EbgX3V0vk+_@f#k-duZr}fKC3ZrIn0faWi%rb} zl*5l43-Mxt>)@U*7+1j!TR7u`S-}MlYIPG%r4GZ@QZAZl++H zyyBp_=AapN97)41N6>9A{^!(~XMFekAUniDJW(@ponG#42P^`&*K!1@Nwhz_j3Ye{ z88Sr3&z}eDZDJ+l2<9pQ{7$HpN0aHC5bf=bFI(Z8xEN)O`9T(lJb!@|XIhCh){6n7cz;EN7mg`9^D-1L1e#g4qK{g>_e^YZSC_Ymd zUBoAhDYe|HK(ODLFtCy9KA(XdlMy#>YsM>`Aa{wb@%+u^*LgoqlHpoVSZc^M!%epv zt_Ybq>5>z>@eO2o$*`|6n;$(A8x6-U)`azK)0Z+R(Mud265JhDho8fWE2c#=abMs+ z76C_hjkoNJ$s3|Ek1ubonsb1pUJ=U*K+|2nyY3k+lY4!Xg@9W?*$HK*Yn=*1SZzLH zKt6Inhd4NK<{gMndVCz*fUmV5(DE#nP2A0dSsan+tV}OR)ylZqFmfR)M03#+q9tK) zvB09>tOyOo2f>v^C@8VWhow=VorNHk1WO;>h7sNd6XM;t^%TYziePY}NlWOTFA}%! z-jUUJmXvrTuQ#!b6{Zn=D5uu4TIS>6Vuv9PE@BlZr;cXBHI$}~=55{4OhiHJE>;*- zfX0fHup0}66Q!*?5vlHEZ1)9cs$9%Mt>p>{zlbMTmsp4kvjT%9j^F{oOPbNU3`5p*V;Pl znTnR@X)!>Lur)MC#7=k4nQIoA@c&5sbgxm!Y?tlD2OpD7c(Rt`OxE)6kkF^>60^ZfGxSM8KdG97aMxW(QWfR9ZS|x#To>zB7*@2)E=)@7mI`V9LV?!zy3^`Lc*3vZYm7 z78@yBXJ z*)803N%u0o9&=r!&DrX}4Hu8BRVZV%NngsUd}xQ;A*eOZ{Tz6e6{M?Dn}h<#tTHH- zRhiwnTbXzPith})? zr({osGQT@YhLr=bjh_IpGGkaa4Z;v2fK?1DtsWpV*2u6bEFiWqp5s`feKkmPc2j8_ zYCFDOXIS2tmXA;wR(cv#{Kg4Eq`EPugrcF$4^Vad@l@%lm%C`w6 zve!yDt9h2_kh6pi@GSaZY97wCfEN-l7?c**a$=h6sl~RO80P9M@-1cmo-#o~GU90g z8^H<^+W9s~NtK#{f=p{u)DldE?7`J8Y^YH2@c5$kMze5}dClUp76--xXb%iWs>$Wm zfvE}sJ52NaR0Xb0p6~^fXtOABMJa5yJwt2t;y~BHt!2$%)Om%uuLp7v2V^r z0|G{;gv6T)A1mt{)6(9YFl=QGVHg>U~TfPHg+!P@_EQh;l|Nt0lNsg%PD_sojssSsg@AI-6zM8{Kxy!Rhe zkaygkBc1qrZj7QL)1<@FAe$L(xWmYB;GWcu+qqiit8qS@NK+MyxSoNF z=)|QF(XhCKG;*=3S{6EW=SAq&Flh!t|NZUz>)Y=>et&hpeg9fE5JNZ(c+X|dFw=Bo zh{Efx`km|D<1K6c*I&JY3%>I5dAFn619nI85BG8nP(-m%CT;||IOR!z--~*K8fcYy zhk6nSBM}Qy&&nA2POmSiPKC1YgsCd&PC=v}F@lpRW$#<~U;X#!!auw8Q0w zT|0ad)oftfuo1CVl4pQKDcGii@#u`#bs#jItIK&?C#^tp$z>ttIX0%Gqk_FjvN5va zom^}_3Q*ZL^h+u+{&zr-;!3q9&tu0(!VFY5!5B9SH+b`YL)$Z1uynM9t#Jvx94+xp zT=rx`&IitI)MQJ8yF19%!a-9l<1{m8vnVaZ}03nwi?Q__ITTtUpzkD-AR-Q8)D zHGqE~eeNXrC^u`hhh_tjtMH^rFe`2>5;w}{nNa0X3ZkbX?;GgnRs$Wh8Rt@DqSBZQ z7Yq;sl*8%Av_TXqlLjiqBEBl>70Ilq3OqSJwYGjXVT?N{WCmnyxZb%?ic`gXBfdz# zP1rGnmRfTgzH!-ETKc-&jY6N1Lq6mKEp?nl{wn)dWRX2bR)xPxDoMOY{9o!3>I*lm z)trcYgMHL~f>pS*e~H#9WiA0==$_PZ6^6IK-sNEM2-Vx-gU8RLagrpTgkQ=qrahsP zy$r@QPY0E{P`vya2K-f|7LS<8puDmI5Xwq8(R?Rh}~>L!AMqwZ91%L3+z6=6tD zZsLph#J-_uA6IbE_8}OfYE<^zI*jXW*I}Hubr{QWl(V%*GDKK&$q?mWm$EfS=Ll{0 z1cv#e(;sYl;-D{yuv>Uck_P3`x!52aZ2lyKqZ0~MJqeQ@^ze}roj74>;7-EQ1X48c zN02s1j-cfDj}3IZIu751eqI{1poBi-tE*EbB4II{wl;BXtJe)cChCWrL&4U26`?iFchkk-nrb41v{YUT9hxR=Bc zvM2-?swSb&=w}k$hW3bRXPHUUEOo32vur;W+R&VK^%Gkx=0dM zkC+Am=EWW%w>fr_87irVjA1n+#59~Nh zadY?e)29|X7gi!-r2xfd02x9ohD(`{UY_tkL*(Fgdmd7Zv?OevuqHITG6;i99uk{V z1~gVMVl0^64I?yR0V%o5vS<_X9bUVndH3JHc`I={wNRKGeuPTs+nzjd65*8wY+OW?$? zqszG9<=E8w38oF~5tyPL;l2gU!PN}k{2dq5Kw8J9F`FejjS3`i|3U=_;)v5BydE(r z`z&fY?Rz3)Q(ccx?V*`I+`7-@`JmFDk;*+n^|t4va))u=HVk{g_+c35^8CXvuD4x> zao+YJ80YeQ!1#JhPlss_t?L{a&?9+5OSr$;1J*Xa?yL7@JU9+AL4qen=N;b_-8aHmx~ zF4Bl_efJHnElQ}b>A!KqF>R^gm?C`#i=b>tIo(&E9v3kMayuI1R#m1j1Ee#b;dT{;<#MDkhIv4CTk`F@e+N`tF+}vG$_xgu-7;3KH ziSgpY=TCmPdjI6v_J^zc?{D?n{gb){N4vuq!n}p<3cWv?g_bQiIi$oC@jwes4yS60 zXqa#|7fn+{4m2W%$_{_U!<5-cxIHVTh>F3h1&6~vg%fpuXSOM=R4p*vY}*mU6KqrA z+HY#MNgToQ7?arFve_%FU1ZNsAsS-bD8t&tro33PcIEIR*)j``GcQT%f~K{r31?=( zQOo0|VeM*ysakO44m1`Vn?*T$2-bk*h^s4 zf;uCIc#QM5VYH`Z<=kVOx3$qOJF1n&Okl-ahPWtf;g1Y5}nFXg=(u2wmXPstAgAo7HH)OY@s)%BKxGI&M z9c~b2T27~8pkA0MeRq-dkVPY5J?>jzJt#_-17fdRBKi$Rao+YJ7_$H?5hpWdS;e^Cb{)of+mcy^mG2Jd%AT>~y1Hk6 z0%l1hxJbw)vxFB`9t#wf;2V_8auP{u7z4K%9LJ*I+;B@p zcds*Z}J zoq{yQ%j>A7m4r=K0*%OZEKpt_o`*nqcX_E?=Ex;*NNStFi5S^DWV&S=3`Xw*$v8`u zS@LMzT_1O3v&@w)mLWTlWOXX}kSC~i`FM=; zwqdkqPGygA-quFDtaJ&+JPWo;@`)JhSe3?&wyQ8M+S+K_G}gHljG3c{OFoXqjkfDB z&ij1`#vCMOItX)fMpE^K^Hf{M3TqR@EW3jEbU59Vp)?qBV*e7x%$GtvLt{gc* z<>}FiT&e2gJj^mSP~pdkW%_ZkK@hagE8z)B>*UI!!FjndHdyI#jty3LoRi8?YF(s_ zP(Mnoye}wDNr}{5qQ25b903~A+pHSsu0VZl{p_Vy2?djkc`A5{Ig(e!IR!-w6$i5a zCJ(P6Mf#fZjo%0b%UZ5J8IjhiC!<7$^6qGDmoA))!nG&T8OTDewmsw8)LvPZ$>|oj z%8Zl6vqE^b?=^25xZs)lE7~iBT%()EUX!ub+6q_6=j5lK%~?Er#jz=_BYE%MbCsaG zwH=$jL0GJwNx)T5ORC}RNe>B=&<3I(bDy?|xKA3yUCAP^cv+LG!q>c|PFdX924Pn^ zOy;|?7^vq}T3bKIT`Ag28H!QSCG~cG9w09FF?n?YDHD)=@4~H}F3IKd?$~PrT@C1L5lpE^iJ z_yq50Vhi;pfST1xAc1f%wqS!z_&C!2kyPNZjA#{d_{%M5tH!fcWWHypHZZwsk2o(e z$M#WmNmVPz#R^-)2%$>Ao^bZTe}mPzZ`2Ux=4<+I!s@K$!jWtG>e;AQ2tRV+KM_a* z(>Y_poFt7**)}j8AMXN#oyEHd!`kme?I`*UZ5P<2vK_&g><|f?w3f@>6OVD;HjH*5 ziX*`|Z)^Q7D*>|CMy9R8ViyEA$6}Sn^|tFU&f7+zFIh)&*#6`zaK@hTWtMR#VkR1S z7q}wyH5goMplndMoDV@+Y*6rX6;&GiJob?Ab0^!>`FU(`mY>I}BKe&6yd?Aq{JbS) z`Kn={%EP^srJo}|=YAMe69rsoM{;P69`}tfJ^dysoJd)&rhbDF5v-B5{A4L>Pmrk? zJQCIuHq;p@YftPfQdYg~8Md`2xdHZvylr3t3t@V~1bF@ciMVJRa6Thv$!J9*uduBJ z=}wq{8*QWQ0s~RDrK2s{hS8p3Aa1lp+lP#nWmI8#>qc8|yKc03+lP%dZ|g=Y(yXri z&DmYUK6%)#5$t3qtznlE!-c6ku**`KWycn>X3}6CyYvl&i>Pkq_^W=b^Vg&Ysq5wC zBuuXqe~ShxBZuHP<*)5AB)*rk$gB+0a|8`ca~`J0w1~Wl3#h=`^H&{c%3mATDNP(x zaO~-5=bm_V0|SgqYB2B@K}%rZ^HnXm51wtWetiAm^UloINd^lEjiMKEiNrX%bAfHk zZiI3Ky=C{ctA#7w2%g4O91BXcya+CQ3qbSmt;F3CX&{tNJOLOEJ9$31G~wr$70unz zgoB$afm87))TTrhUyhDg`MIq)Lnrkp*yYD0_!D8fN3cc#LQfxgK$L@nPmTQj2z&=# zMq^>~VZYOKD^54-d-N$X%4-~G_<{62V;=IMeO zd{XMj6Jee{_N9h@M^Zx~uE*X_{yL{2ajhTTz5n=+E{yf-izjcs`&Qyu-CD@JRUW)& zD#a@&`Xd)S6OJ?I1)k&hAXnuD+Z7jrbJ%f@k=v<91!9jC4;Zy4o8uPqZfzWe@RBzod6a&Tho9!p8?{E*W8#S*6U3^IN3;;@2{R}8U!ZuT(}{NokRul` zayug#WREW{ft8>Xc3+!T3CXd5iIoVZobRrSZ+qXBb-+ue~-(1~)xMgirT=hq6VVIy(hSKaV zGL#ho*E!(0c8OCTc3nM9Lu-;nP}xgnc$wt*1{rrF?&`dxg^pOF101<5;EtZpnmj3kqi0K> zr|`h^?ID%}e7cUYNYGWmST@RngiFlWkX#5&M2F^1PZ-Pl+qYM5ZtXtK_VevOKl#Ix z-#q!lE%$Q%Wr3-TA^mRcOpQD&GL^r+`grv>@AW1JJ=T+!=nNa=xEBZ%m4r3v7G|3g z^3-B^2BRjwAz}n*fv;@WB~@wR*AWDFa&m3H0$VbatQ^BJj*}OaBAoMyLYn9xqw_+U z+Yo6|mD_8c+S6AA-9taMqrZh3LW#jlv#P9I$g1IrI+dP~@V5Fww(QKc`HQP7S;#7X zzkYjj`{UPgDW|qD5bP%dTuR;yTLK%C?Ni8!^qOyCByYc_MZH~I(-Cruz;LMyj%RP3F$U$XXU6-_cxDMm8PD z7V9^)ZNruF)_4dnrRA#$a22*F$8j>RbGGQomaL}tw;ygl{$pHDwK-uqMaYT=XLWdf zly#l6XQ8;uoXQ^fsd;9E9Ov?(JTt;*uP^1makAX{s%^Dwv=OIteHmX8iAz)xd8Ic# zQr2Sa5zPpeVCcAR)5kvpZ$blaN$-NGJ=(cyHT4g+4cxqc_njTFpTBtWx3_P<{rKw1 zvnT)k_Vu@~K0c8E;~EuZh4JQIA^=3=q^KPuUhQOIpJQ0W9#@$`1}RcC%V-w4!DqYU z)t=XAMgK<9=N;RZH~3xTh4}{a%HTVOuJ7QPIg+bJ^-O1a_p@p|OpKhK5_2Ad^_LW7 zsVX9tgy!APNYO$aCyqUV6PG`;*5V6cViHZY6SG*$czB%dc1WI)_v=M0_v))eNZ@NG zbOM_;i72@FPfWtF>-NDzVK!`xgOV#;(syerZlb?Q^e{|v*;4qCe|~h(JmLgJ=#>WY z0+HivzmeSHT85*><&CMx+6Oo&my=IV0BzJzS7SS^Fx6Gqn1;f7ag$w8PP<-VuGaV6YH!_C2~whYip+@r6dB)sM-MD_jnw_K&f zV+dsTzAI&ZAb>84@aZH)o&f&E*Mz_kqd?x|#n)JmfRVdxVligk9$+z!sJSVWy7GJOu5e39A12-#leT5faLjhqw{`FWWW0m5IuZv~KqVC04l1FM! zWbt>)HzHys63XMdvNDO~nU{GSBQWp}J@ImCK<=7xS#z;pXJ5iV1F+<0)EkLj3_S}G z#f2C4<-`N8*Z5nhaYa=|8Vv3^}Dwp+UXxt@DV7Pe|hmzvR6CFQ}9xz*sv<(XeMwl zm^nD!sz8LrV*1AhIuf-bK{#Hy>w?M+>>W>~?`}jYKsvbLLZ&>EeVWi<_Pd->5?AIK zk!O@_+PyrX@@Pg$##4qz0###n@<<0@c^x}tl%XuvlP5X+gVolS5`7)p_@pmL*rZhn zJV@cVR<$C}yFy=qNAlgh*6sP91V)6B1ddoMVNpmf5Z7vR;Ehp203}$@_mh+ukBnu; zBg-OqWK!gLB-uWFZ-FC5j6#X*y;(E1)f}BT&N`Vatz=bm7TmuKM^c2~eA!7XS&w@wFmgGSmc%)pJ-OFC&(BF|RAGaWH zd?3=4(o2O&5|9Fu3=(kjRczA5%G1SH!&T%9zk?qM2kpqOTn7T1c3kKAb7Ni?H95Qm z0QanW1KSyzG&jSliK&b*(MgennPM0jwWKOd#5J} z|3_@afx*jT1nqL-A_2QPMS(o>KnjLjW zz?0+(uGR^`ObaoxB7)0Uh-zpJ#Rq;d{#kxDX&r~sgb_zlsp61~1F2LAL|risLWP-x zd`x5b1t6bC8|2j`*;ICIvVUB|$PxNDWP8bwSC=4orq=9?vX|6WAJ-ud7xyi2M z87v0n?F#KdA(96VdK{E-%{e1w7MhH-GN=TS4vN)slILeCGbPJqHdDo*ND#4Rrmo{b zxN1<$LUT~(PY14M$)O{R5dnK)@l{>wmveGR{BCmYHJrPj?+oWS#i7>3vFe$nxTy(PxPMaJIRTjnyG4=M2VzP1dL9Z@dB zC2sMMy>)g>&Arv_ntVOX(^MPOFR-`fgIc?{7G^38>iCPUy|}mLgIc?{Y*4HAR`-jJ z&+jb+H$A`FLQ`XJk;Lb$%rw-N;m^Of&X1|Jw|r)z#dB+48`RIgwPD(tP( zFS_<@Z&e1ha&N^!t=e1tFFHOo-V`s>lQ>2)WfeDaB)T5Uhy5qN>U<=-GFL26O^Dw2 z#sk#v}zais*tsl(}frtXD@K=?CVlH!Bem6 z5CSYnb_7&n+9LI(7R>GuMJ&BdA#EfYtshwfLw=s{m}QMz!9q`*`bt0utCTA*5+yiE zo-nZY4J6cBzb;WgDF=M#WD1k=fA_Ej> zA`7pOV3^?g@(;$jK)IY6IX=bIqH+2tiaZQ#;R=><1(Ld@!;M&qL(M%G=lq3U;%^2~ zG{9u9#ih}M^%#m4l=88-Kr*pzhnpcu3=y6}EZ?W<6^BL_9ru@Altxa(G%zij6LJ5Z z+tD)M7+kiNSB&^A79o2K17zK0FJ6q#wrt>T_sz2g58|><33rjZx!wB$t^-%-x;9*5 zLy}93#p-M^o)>#qjWPn^H)cu8X*@hE4ZCPNhfno;yVeVq>I!lJxCd#xJPH4#JS=>+eJa7p@>_kyxU50CA^%VpsqF%VYP6nl0+`4KMuLP z=8q#|ELRTq>XlvSirJDksT=r++IFX8C3Xqd%>vR8_Bc*@Z7sMLInP6Ou@Bc988jD1 zWge12&&S-ZbKb*57xLgr(gptf8C7{2S5`%ag&Qf$0Dp`GzqUMsM${S*oxV+HG;P*N zNZPJ*cDQk!6oby(wzCn5e{PVc$q(x5v=Oy|#1Yl%l#8TR$1Q9d5#+782z8ARw;~qQ zY<9|h(XL}N$SCW+T^ny3NE~miPC4FM9f$BLMkdaJ79%5^;~|V}Gc*|)-n+=$0!vjF z3iRM2wghhoPbBu>cy#=TS&*Tgb9Hg|mFO9N?zWBBS5fy^MHZNKlPtdgfA8duJ8$n) z$s)5mBTM0{#i_f7EQMP$%+cmC$6jN#u)rRA?6p=4_7OrbiJ^{UIpFnfxvz7TzdJ$A zuu^!mOO8DmXPTUCa_-pvS+RGBQW@;(VVfXt|ID@yagAgG zl(@!%B-MSxB1weGPGpf4H2I;vNd8p!O;{u(gp{+*FB0CRbMnuOndCc1)(KY&RJB>y zI5%~guvhCM^dEWSbhb{o!VpMOxo)A1A`gU-MPdfH_QomPRDroR3G&B2bL=S22-4a$ z+uvcBQ9T4%U*f8Q!C_92pzs!f3ajDNk3uU#FxoT;2hk^NATMLxZ-VspDZ+h8lQnxtn99(W>;qGo z{>;P^V=5(0TH7cUPlT_mpvkd*6o*yuQ!v_}N335BbF8I^68ux& zMnQT*(|@3(B9fa+SXm9!2$5a4e`Z_AB)5VhYj%*xZYxOA;Ix0TJIG|Wl_W_wkTBXn zl8SsE7;O>G)7~{+!2M&(_r_=olB_v8e zip{2bh(qj?sPhX*a5bMwJr)W3F=}-DPacaz*gls+B?5%g%z0Pf+wmK>|D5buWD9of zM9sz%rmhngf#bhz&bQ;BP;ZKB7rrgGb1_V)O5$O04MmjIugfh3Q)wVcxv?X>*>prA zsf)~)%%XYX2&J9WhKv9S?R?4X&5qa(igx0!B78|`n6&jJjXp!X5iv<(-A~&2l14ij zV~m7y^nkC)%yq=5?Y94VLdO837z+Yf*ecDjuolQSplc|u1z+L_B!<{@d#+&N zNQAuw-YUDQ#wt7}Y6Hl}VC-l>u0?HVz(OU$%hcQ?9D*)_71D{cRpccLP8JyAqB!Oe zUapHgK7cMJefj~>urZ2SP=xQ|ArW5mG=}TF>v{^2U4+~H-S_t&U;htD`E_~s{@t5@ z{P_KM_qUP}!Tj|(*+}llo_)laUXaoE=E*Y>ct3NWdiUw}$+K2pT%LbOUjQVorh&J` zYOJG>n+E@1$?o@oltXXHHuQr8sK~!YM?8X^BE2F=CCr@W zIDKOoDH=%Sfj=yi!2!8;PNG)4?Zg)FOlm0E@P07i5zNrFm2Eo?(UIOoOtqG^GE$%n zC+=MKzQ;{R`XmZ~Z+L|g;M4{O-#TK$ugn~3nMJS8Oa-f4XjF~H}%(7JUzNeU_!m#@D%2Gl8+2g>&X2=$Ma@Mot zdwmg%gn>lAU@qFN%2Gj=2|F)iiX4$jG-GM!;_+UPree6c(WHsP%R6#!lQ!)o->*PH zf>epiq{i*t7u%Rhu@rB-Iz~FS54hb(MvZX9(%|Mcfh&C{>A&RD{vhvOi6xn5JlNqR zEwFva8e(tC3*S~I`gmEUJDrF1c!%5SC3_#fc7koBB|H}Z6rN*?9D?jV*!CqFf4Pj6 zdpvPHlx)DRNA|S7j<7MjLWyvtFeWPleyxm*BLmsgd4N1X)4ksz8Cj235^xP%Lu#=K zWK61@3)e`>)P`YYff}JlqqgB?CCW7dwxRTsa@#nrp}09-s~RT3j-yM~&Wy8T_hj>N zTqFO&S|Hg^U{ zax+FAJu$qj1o)vVD+7L|%&?5$*obUo?Mps-^dtwE0gxPE*%t#7hs|w0tAflZTt4(4 z@_@Ta2hD+cnXP7WC>DE%YozKWEhqt zdpF68QC^&pyc*-|<*wFA#$8-HAQxCI$B%#^*^m>tBe{$PJ0Wu;bh;(4o@TB*LW_O> z1*||w(;rAdy5}b$5lJnF;}vBj8!I_BzblB-;9`ak;};u8T-Q3~-Z?S32XzKP(oi!= zf*Ak5?XK$+hVXgKAnpdiST8utu}sdD&8c#(WSE&U+@ze#n9tRsTlCvI>6G{A^bHO& zDl5WJk|#Zll9R*zM8M+h$Pzq2(W^WE<^<;+ROH!)-SzQ^5KWf$#saiFzh^LmBriwE zHgcST*+A;JVdDR@Ic_d>3`uTsifcfutOutL=7z`D7bvK2Ztp*SeQ|SFzvKdBcP}y3 zNUQ9kROByq0NAvM5ocB0)&31_B4sl>}SjGSfhOHX0?G5p(|IND9`8TQ)^YvDDp>d|4xD zT1`n7$4+kiityVVhJjViCvq(I^6rRz3iwjk#&`DP29yH6vmY~jtG^n)`LAxQ9G9d_ z-t7>!#2SvSM%q=f;}P+WS1I7jMdLQU`H!B20pI*bH#3pG)n5(Y{8tB9ryEOF8=cyZ zM!x*Va=P8}22_~yU5v3~LfM)1@POmg4tWng$Y~llbim(?B^0Unit;m`LG#@(qQVq4Z7Nfv4jXosE4x+Ymbi zG3&?~ZXEVCG)ydhT%k{TBsq`nf7dR23(fR?w+l6l&4NRx69XCHQ;ucwZE0izpF<&O zz_*#VDM3scU~5Sl@NJ<7eNq^GIcdPRg$9Xp>HKgd080)AM<)2Y`?}arCRB%=;KW0o zODIWoWR_$Md~*#-LxpSsS+0_=A$#l&=(sHpNxE~n`lA>**|K%Gyh`cMdhd{pkM^D>8zWhhu($J~>8qvvrb%13n3T&i zae9vYDJ)2Ly*QGvT1Zx8-h32e&*|n0^QM?3Mjnmh_dVtd%1Me(oih78mX46{Gn!Fy z1ZvpvD#apAiogH}BJalh;|*Tz*Vk7c#j{;EBxu(LUAj)@ZtgH7ddFsKum&{I)s60IYIasZa>%N&vzufSpD@;kf$d_-nza$T^UM)0tWwgJnbB<0$;Svhn_ht_S!b~}1l96dWDLkqxm zzo(mz+s)mO4vt{|^eYgodG)|KB{Op;%ba$L3_mUJWZ433@N*@>P^eOL|C~cZ#vAq3e?b4aVJp(ML;t(8C6A4hDL=cD=Wcb zgsGQF)`g4EsNNb^6_}%eS{D@Jlzk2Ncr8n|ULd9vgiI_nPbIdlQI1Mei?YNbEmE&C zrNkl?l9(G;oh_g!N>TeU;}pz6t@RBVj}VKyUO6hxfTkmDf7Uq7S1K13FAv%+mDx3A z5JzN$nyvta=G$*hNr5qWvy&cTmte|jquz1(&}-!&C*a^pTMqX)6JV!Ka3UpR)EqYI zPFX%<`%_udOdgNCa0;$VviCC;_p`cgPYS>lrT@!wdDp%?)Wg*sgIAGm-^pbl$w=yo z0^maDqp$U;jICsHh=%Gn$)lHwmkE^R0ui9w-O{aX1y*8B+MV&p!R0C!RThQ~6tb%a zT6*g8_iKX{3Fpx)ta=HZS=~E!VLm(ZE6KdDe-&NaRv($!Y_SUYuHeq5`>ZXIU%}wu zS<88^{Dg^ES;m7F@0lHIi|_6c;lLhg1bFpcAugPEEWPk!OPkeBnZID+KG)_%fT>Cz z4q^!m2=D8YEGr-Mw>I4rH$w^jQ4tA22vD>H#`t9OV0Ul4zs$$TXwFY^?TkgpR04YU04RV z%6^_ErgM_b2APaA0-22;W7URX9}o@@CsRyH#bO5gfB+Oyr;O1DQO}0}z0XU|5zgp? zPKLPs=0uJWj4(7AUCvPJLOHXL%hnXfo zNr3iqW4N3N^deRs6jv2xJPqMl1cgxr@*;d6E|FpDhuaVtggg7+sg{lue$e4&Q*`+{ z76K3}XL^^S)W9-22Fs0H11}Q?MohSfVk1o5Hz=)iG?9~Az=@LJ*44KY<-1T3#hPS$ zXvvt1LUZBs&{wm|n1`?1_DmJEBcv#?6pZq=kq&Zmk9*q~&5IgE3b^K7j3&fe4|xfL zPVd57x3)|G6{`cG4h-M-s`1C%$V?{`f;<>Lj%eEieW7kIM*Hmr%}arlcTLdIjE&mM zNOi4BY}Tj{$MEF?$ya|l8_SbV0walIwrZQE&-}cS;hZ~s`k-ZTf{|d30G1!2BQ@R_ z@XBN?2BnA_BSA(rczq5sOGKba|pmuTynZp-`ew z;i6B2V$LZP5@XXbhcyX%Bj$T2qdP^7?9m~N=zlQJ+su{69y7&^iZ>@Ab^ARyWunjl z<<#+(;759fORjV1(Pf*>83wmrOB=?qqT9aoO7FQ))=nl)j zGyQ*a^xdD){Z>Wti%a*HUxzUmIo(@_2}YT@a`!B!QTLl^m;%7Mr2$Pb4fao?9H*Dc zE&0hb4{{uf=8DCg()$-l83+-7u1$J(OxG9M-=rV)uxboMU!?Kj(j5(ak$wgxX6J(i zIHRS={4zsj3|vey$YpYQTExwRa7*r49c|0k8^3n&CsU(ugFZ@LqOA1kV>S8bYe{Hl;RWNArmhe{Eaz zH2%K3&@!yPWKHY3kildu%dKsqRX2?6L2Q;6au;Z0@>&(FDvR^d&}d>loDcC#2;*Wsu?0MnyC1ncXkzu;_bJ>CFYWZ_ zz4>Kq-HME$@u0VzRX->gamcEDjkr^le{|>H`7(GB7z^VjnBcV^`p}xE+vNgX7LS)6;ah%-P?Bu(qI%YHQ5gL_v9SnhF)o}t5RqcymYIR(5 z!>8KGJmNiBHVjF??4gs+wnmhuw4{!pcqN3_{wP zDJid_lsa?fw8`T;qS0l)gm&dXd&B|dl$W;p;H|+FdlT$Vm!S}rNc}o zUo_0^*Wy|z3%sBI(6ZO2KT$aD+q?A%puq9twXfPzl<)L~;;y&C04YmL&&)w$;kun* zM(H18KG|ayLCu%EcUjZa(lv*0t{QcNhnn?i;XE3)i~ z*7I_YEWD@ziw?@Pk4v;r=E2ZAWlK_E5v-G{z2T{5K(lLA}Q8MPOJEK6Ofcf zVTzWOGHE6vb3>M!ekh@=bpI2*%6%$HVvpM~lTU)~JAd#R5)Nag`d8~s-k6D^VcY3N zCm}g}wTi+yT069*Hf^RwnI-?F>Xn| z@LZP{=Dqf(9hs!V-Y+YaQdt}TMk$*~G4U#b2;JDl))Nc_(YO&bh(hsZRCX=f!D|MA zwRRl|YtYn?!Kq`g#jty~`U1%c-MF2>>}{vDnI3v})Yahc zFjL%6hN)pVFWxOg4>+6$qZ_;4O|=CD9X1=?OoIcaG0Fn%AaU zsczrVeVcWh0>Hg`>?ktjoKU>NS`6os(nN7CXW>jO62Sd>T@K&Y6%f|g)3veZ)Bv#N z+!zo9xtzsRkd9l%yIPIq1y&LklfV zWE$zr<_8j&-Zu()_$|e|{ZrSUIb3+jVeYux__hWj@lRT*tWAwkkED;^aFbI-MZ7>g z)U|@wv{E@7EmGMcBExp;6Kr#4a_ zjZ9HjZppHec4^6Hvqgr*bp<%>Lw^WxJe9IbRpVMf4{tjE8?<6lIC#rl>rgkS08tq!!A*@$}uc?0>mZwC#IB+E2$AcDQO zk3fWVtm0~S?80En*H`TN<8!my)^Rw()`os>&)emGwsjKR*@*FvW`gv)MLUiyJ@?*i z;YP+KVn=5~B((@RFxdkrASoUvHr*k?A6rj$=THeG>WBdwdBSjcgh@Rm4+PE^DlqX?x4()1@TbA7po3c%*#(>5IKj$f_ndiWY? zM}`0{d1hbRuHFA@Z_(eQdv9|g&jU@@-l---^Et$FI0s3nMJx@kaAeAV`wfUJi1?z6 z(0s{pq%fdA7z8@B(e^<5PUa3LDYzxjdtor>h3ul=(SX}HGFDdV72d`fMGm1p z>=0^s_lPqjg}}+et~Dh0Xv!L7N~N}DU1V%87@Q4G8<(zSD^Oy5D2jfaiGIR}LCO)~ z>Rtv$-ELHqU8T$j1uZ=yNiHQP#y>eJtVUNrB53wSKgk=ltt6W=QfPV6AkZbqj=~YP z^P8M15ndiH{^h~fsoB|1t?_KhIhNWrQM7#@FCLkit`2W&>Yy8v(07T9Qs`qRsJ2tP z?n$IGc4zYA2Zb_jJ(1@ABz#dx>-J;9lg|6UL0En#mJmO3+1GyEE0M;q_M+9;PAH>w ze%W{4nG>!K=u1y_@$oPZ3H2Vzrt~4+v>&aC&5YKYR!hcdufrO;1u%(rj1935kn zQ_AYDL)1*u_(MMs}64EseDhk z0&+OjzOnHXsI~~zU@Magm=Ob?4^mj*o!hV#1Y!^dgRQ;d!(iJIjE%d1t zN6Q$`ZR?`g$st2R+FK6K*a7*#+eXD>e0;zDk!T~kiN9AC-P<<@{CUJL^q;v4WNw$ z*n*PI(nm@-7Ys$x-lA@DyBli0jnOAFHia%K0%18#;YRSnyTa6FjW*1w2)nBCyUcLA z|Lmv-1i)c|?>Uq3l;Gk8it#yn&&qmIQ|UG77uY(JC+K-KKv*fuyH`Of)a3b!XiwQc z*wWZ4zml-Lj0 zGVLML*PFRO-rOLkL&u{{G!x~oG;}%8j!Twn=cTt15ofTCj~7~3@Zy8ZEJ@065qho5j0?eR zi%t#jg5=_$LM5H(+Z2tWFi%~r0A#@)fT0ySNBT6A1Vf~2GTvJOw~Q(Zavd#2Xc3#s zWRrq>ma9-2Bao%eydKU}_(tubaYqp0=p>Jqjt<0@@~K;ZAl%kOw(~~$My*NP^(Ps? zddqh`rjl}sirR6HX zVE1@w@;KDgm3Dvr0u!_NO!D20yFmOUSm#nz#jb#2SyKa^Inf=K(^V=!CrFs}`8Y5e z^mcF?ft+@;bnjUyvbfC4LnBhIJ;T-MCxpJD{PVp~vV0Vcz|pbZEFQ?iZ1)d%1o%|O z9p=oTAws@H)^+!(qje9PeBZY_Gviq5;gW*+7~tX7r>6RP#du9|ua4MNgn<(yj<|8g zE@wWCFbP_cF8UDK^k8YvXSlWY71g??n=Fe-yTstA6KWu|U`>B?{nwX+-sK!BuHqT} zydBYUI7Q^2jHVr=Dy17|aWj)P`1U9`3B2**9WH)>w`NXiyaHnQ^*?nXoxIRP555fW zbL)2)x2Hk?94SeI#0Z0A3$%n6sqY34l0FbR_ILfx^KE>c#zKkAWokN#u3vs~cd`#U z8#-w_)DCYhSt$fOi>JJ>E;?mSa->pU;Hu*lLUELtYqPOOTjv(xv$LJ5%6_;!Mm6j zL$e-600;{)(oZq=-$XZZZiN!WJ-ea?z4h-7vN`*O^pZxi5T(>0iH7nnuU_xI{;(%p z&86xL9?FGdy^F2dKTKLU7d3#?Y!ve0gL!?jdH>lg!z`1VL_4+*>l1Lxf>zOgkTVA^ga;J&|x6A^?HrV5_9!hSmh{dZ1EFw;C47sZ2bNMCg*&l zO_{m%8~p?p<*65*vfzQ`5G8XpljrN+s z_F{JQxk2eDmG&hFFk_)pbQq5M)3bVdBC}qcuSN43$`Dz{>!8%`26S4vZ6?*ay$ZGe zHNIexU+8dJSzm=CCl(q_x{8m+SzyRIrOsxk@8T>r5+s@i^jbU$cHZm2GRDUNLyt2s z^T*CfY5E73*(`1&S5kaJylEMHRJx1aL{7aW&e_w3qR}k-+7srU{o@bJ@_Kc^1!sGf zZJ}V>CbozCp~PaYh^35GqqT)FWAV_#j0UkZs{($8Jcg5uu>dofI)rTRnp5FIKapB=y?RvczFiIcr|dO-p1=}Sf`eYjGI^rmlPo$X_xNpWJT0U;T5mOz&*KX^tXdwa*Lg^7( z{WaarI*{1Z{usU1m(StR1D#4rtW>|VV`vN#+Y|?0^uTm9Dza-KAE7;^3C<-j@~7mPrdgn>Ix`Iqt=Kvw-XwCVvg99F%${1=}HnMQx+ z!W_kOA}5M3QbemF4gweMaHn95M&tHrXmvM~nNo?ls`MC8h80s@Uufl7$)7fi9L}>P zd0ZdcM^qlgM5%;#+UGW$;%iVthU>KX8}*n-aU?WV8=TTOVosdRD^pSJB)sm|)XUQQ z*9Jx^3!7v`q*b)h*$K%b6QWKqHqJTTYL07EongHX2sx_`SyVNDp;L?+4h@OCLLzDZ z?1e9O8+e`YwI(UpVo)e#CDB8tEh`lB%mKZqyF$Sz$74InwBm&x0-0Ki);IP`7W=q= zKYnX%CE&j%Tx&c-@lzJ;f6%tnh8P;RTETAynu(cVj!22 z7Mo1-V7@_#QbJ8)P+zbc!C z5x9tW^6X_aT><2VMjI7Qj-c}{%%m!0J5&_y0m{t5-_+&<#LBFpY33THp5_zD->sFE z5;#X11|gKFuHWX>dfTj>Pj@T&sz420TrxB5TCCq6J|OCj%T%Gw>`nh+>Hci}A@}~n z=>6xDnT3^&>Cf%II{(4^t?K1yM#!LOWbxP4+05RRkmYX{aAh+W2RCOEGZ#YkzXc)= z_O74zT?qd$hkw$9%bA&483{Xh66!F13YeG}83{Sr81QDBV-V9uyt@&aWpb9BmAQ#;=)YG^sjvv zCPI#XhWsQa|McdskyKSV2$?wlk~4@fefsftQ&p9fkm;YPhh9`4k7d3t}y?t#r(GidjCe|uP^@_n*RvFKMwt4>#t|U{3k{Z&ZaJ( zq5CWDpJ8YG6FE@_J0mOmzoblrpHD%|^Ydw$e)e6R-OT>Jd}@ii*_*gpIoK02{~gu; zaQ2U}e|`9e5T5y;xXKeU5i$tWVkY5iv6bHuAW9US>{z@dhZg zAVwL8#Md7O2U07uvq+B6h`*x1fAd{EG{6{2Iu8J!u{oXg-c$Mt;Ui3OgX~aVAo`hu zOFzFln~6m?DTiV95xi(Q`*AZqmPXV$I}OI3+Evx&_~;&TpMKRI2-zv(n=t|xg)axidRb=1jpDX;$;3?9~9z?u&)j!jF=}jP>6gzBR`a}NyNb5 z4RI0SeTe4hIE-ZA%9ma2f`H&_FcWcI)B~4YqOj+P(z&0StXt?nbJ+}#u-J*oM_TPW zqhDAkKj>=-;l~cxFqzAJZWQzd|qy;UfluaQy(%O#sC~ z(KbZTfy^NblY_R{52U4>4rqJW<2)`xvy26c+YfT(WWlMyNoEEns3Fh_zaI^SyPN6p zyY}-cdd4m20otn#gpGsg1H#1+W(9Q}CkvhF!CgTJP{zU?5Af_lw-_a{K%{GR8~nl= z*K`|iJr+vy=&VO_l8{c)0xp<6^c6FXXnj8~xq~!12Ra)AOXxX)ww;|SM+ppf-`Szv zR`!HqI?>^cpm{31CGNrBw`f+60zvx*el3@+<5`9E0DBp8)Rr`< zLX9bhtJyg^@nS;xNEhki$8q0BLbJju_WU^`k%Q8n7m)?@heKCD2jw!A1{DG&2QW$x zpuko6nXp5q9U`BH;4Bh<1OnA(5Eto{CWt?d$uLBVw;k+&7>A#d0nmD*G9`ZhG!Fln z9Yk}Er(FbLrh(%8s1nRuMl8#Rxk-d5q6_8~`{@`EgO%TjM7Jj|=p$8$Z|Gjl>junt zvY0hJ-aJ~gA;tb}Q_+e+3Zs@OLGo}R4HkwpUt9?vY@EbEJV@DRiD(JP2B9+VF6VB0 z9(IXt$)QSqiJtR1L{kBmEDmK-6lNAUqi$I^hdk?`EYC7@x|LvA&h&AVD{ItokYkgw zC^7!2CP1)awrq-kzHzw(U9a+AhyWwAeP~%!)Uqg zOjTqPP%esO;%bo0$k}AmI|4aia~ZD0Ty@N1z~p1(dXgVXF%_vCzKYe@<(u%u1h{u( zpi&;HBf~yG9t4pX6_y{iZ;#1DU%i#9%Y{^Jo;IB?$^sV@tr-oKa}#p7L$SZqLu92g zP=xaBPG3W(r-xBZBV=F=v3jA@<8k8=1;{XI6x#?!&G_vIXbq z!&ijB{%+(5+Y~gv4dDo-3!b}8|C)CjL}7^71vY>gHp|Fe5_x1oo~MuoEBzgXe=9+YM5`7y25Qs(*jdD&@+@(KfeNgitP?@()V01P}j;CSh=m;hGZYA z(6`~ljvqI&*LVf%%da1kzEgQQ=?SMF&#_y2CF4o487t5i4k<{9!W}}jBiM!lEQ*>a zT#TY@%%miwio_-gS_OPI2v?YmNw}Gys+??^1kV^Wb=&N{;G~>XJ%`G%3`o zRHqu81&(?A4eBMGns@Ps(1&QEx}yf9kENHS`K7OCSJ!###5VpX!n7dH4;Ig1maiP)sb+c4xzHSb3Zrdis>U4>-=t?np zhIJWsX5Y%cq+Q}B$GjF@ZCpFLD79Rz{4RFMtXjwp?#yZ}axHXiac!iz(+v`j1}{#& zWy(5tfLo8-!7bKxwF6`Wtpl!O+0EccZti9o<+s{zyoHkmpUUY4gt_TCgGzwxM-Q-- zp$(!*-FL(AKCa+EDedGg@+)#RS#I7V3Xz{PON>j71v~Zw@B>67CPj!Ok+bMUEaITU z+{5p~nZr0S3YZVsmRXNk5wh$VIjmk23+4@(s}qwGjT1sDBJ+wWnMd^V<0S*;OXg7K z-^?K<&$DSVJF`o(9T**02$~q{GmMmsoxbl<$cV{!$P^rqkGqd+s4uBYsgGC3Y29g^ zXt`8HS0iX{H5RNtu1>U;*SB+Jaj1 z?OVH@OXoq&0sdLf+*=pKkV(e~a=dyxM_x_dQ(ib9;KBLw$MxF9;xD^1y35VIx0#WN z@RP>z()IMC?8BE-ht~XZg5}!b3k!e?pa)R>L2_^EPTraK=E`l;;rcv#R13ljiVmIw zO$s6cLIc78VFgA8W(VhuXp5eM*Rn3!glTz5UPkw)Cs{J&lnqxjLx?vL><$@)GhTvL0z@S?%-%K9?U8Vk`_~IHb`@ z*~*qwkyOo8B8@@jb~%gL$4T0GpZ~4#cHcRLae+NFo#x`#aHID)Ph^$WR$hB=^7u(< z59eWtZ?W0p72E~1Nxq5FBLD#HHP|N6r0`5RPwt`bW*DF_@e}WzQP3pAMFfv%iOMqz zGe4j3O+`y><~R{?SHADV4b5rFF|QBfl<{R>=4@qr$QqPara#KN-~#Nu3R$%^u_KIU z=Pswzf`9a#CajL~r{AGIqN`$b=_j;pOBQ$+wAB@jwpfbI7VU2D)?y^Vc)_eo%gUss z-qIQ~tTkim@H#lHBj1pj$#|!S(QdTLT4^mc5XfkKJCQcWq^Juu>eJS5A-RYFsA?w-gS$nI12t%-b zXL~Y$=+x_se*n98eZ_sa*$D3nSMVQuInZ-ht-Up<#LwidIB_|*IYT(_efC`1+oVXM z7yv!!E&pgaE1#Ud@PGK|HgP;OykI$ML;zIz)_nM0L|ZYeg%yV5cTIS4y;kftzn}Oy z{M>5lmGgeF^z7qV>zoA0zc(KEJ-+x5IP?2Ha?t5r;{H$M{ulK61IJic*g5_SC;khe zKVh?!h={O}iks^5Vn!ii19P+@|DjF+LV z5fz8~ltjf2Hezg`E~?pXHnZMFw6<1%wJxt(T=*?_04cr(f%Us;0tc`bXri8<^ywm_ zj1NA5U?Kv;YXX1YF*gU3coYR$c0Xqr|nfFcUk%XBocFokbVJz z7F%TJ{UnM!07+449KvG$0Q+&9O2r(;#Ef15Raiy5H&|IhK)5zF&Z?HHFp1 znitclxS2(^QxpSMpqoLqZBgV4Vb-lGl?8P0Ksi7Em?I0sh?;3I@r%5`Hj-Hk73)0m z8UzS3Wss}O89w@n%Q6qhv&3RQ3M^8}xCCiA!|LGosQL3%qso&p6HUs1+)jk2?DTCD zY#bv-K&?ZX7%-WZU8ZH?SvBt2nby*w2#h@OrF-3?P(Q5 zH#*QgsdhGAV^69YP?Oyk!GoPDg$qKUYjB{Z&72$jFkAhk!ChDinCR0}TNmJb8+w}G z954An4nKOsxE?ac&tKGeN-zWhNu;>9VX&taXWJ_J05MOaP-BWxpdUM67an4;ydlOjbyXRkRgi z+XsT20O9%-nl+G~5rozd4mW_s5pIZAdQ> zAw^D9&>@k?1jHo4ry24xOzPl@0ue>NTM}374oJP==dTbmgsflQfc-*{GC<~y;8uWI z0#@sIa3Ll7$G1g2(Q+U#zOU~{x1saHxAtS~;9LUHh8pxILkf;Ug3gL-LgHMB3MXii z0wWRGBoY^cl#690z!&4R$4VwT?Sp;^FEGl$L?6c53)hO-F;+KPGL~js%rMDNm&G__ zH%DX%d4#kd?9ZNT;ZWwQH%Io32wQmmo7Y@h zlUx(`n%Np7o&-nn%Y5?D&@I95?pr7`GBce6$^+a3?1RQ1%3t~!4If#QvA)G5$8^TT z->@FGPw!2yv)5Ybe{@l$K2h6K`=%17nx_J%##hLyIV_?s=2qBMC@e5qlUo~EuUfw> z(pSb#mri@+P3Ic0e(b-7AJ=t6dbd4W-whn|9VcUFVfSETVXI-MvktRiW!YzqWgTSw zVjXF+)P`vW(>~Dt)vV8$nt48%U`@v$yaSs)~IPwB;PoNqL)Ok z!y(kF+p6LrtRTan$zV{cbWy;slwX}++$+(m1NiZMhH%X=S}41(tRSkO*C@}ZXvc)4 zBC&Fox=6c-XPDEZ#VP8Fe@}3jHl;urE4CJ4T8&+u;ehZR?M%FYnK zMWmD8J8e@}F?$dcJ{0xyQl(qDT2oY0a8pGvuo#yZ3v`&yvrap0xXy|6rt~oeo{j|z zYU`@}u;)_f+~ZJQ}@$ytzMb+$>-IdR~4}0OJI!g8B_z3?>ey4#5w@422Bt z1~$+K+}9aIz%XY(s=I&=BK#`i9?A>F2CIkm?1D*8%UVT#>eZ&^>F?~k>h4_rFt;HS zzIVC{y?0K}qMO!uy5`(`P(D6glVH%N5 zRY=og)4VX5QB)m`9t$7EXHLvo#3<77!)ozd4W&?UHc^(+Ma#T1Bko=HPBMlxn=Duh zrqQ}#(?Y^5|A*29(&6oX*1r8X-q_99i$9w=n}?f4kNbPrYaiRW4SaRzQjP9*>V$vvVd{8Qn6j_SeAHg_@xUP0b9tke;V0l-)qWmh3 zDBkk#TV}MQvz%U z@V^!w4!w$w#0clsaOLl4+Mmm4@|GsziM|bU+;c-RZFk5sNvG%@yqq=J_~z` zUBd6oOEWn2w>oORuBRqEJQhizQBYx=bPo5$?0Ap1+)TnUWGvQ+v4N0d3pEk>DOh$>Bb0)_^>t+hL6;z zT7?1d>GEkB@h0(OiE4@In5UQx@A})J%kh}>`MuCys`r+M)Y-~>e~*`L3*)iDOPkq~ zOabZ5vL3GIrF*+yroZ>+J*+NDN~1O_dj#$S-wIw#ZbuT2P8A*%QuAX4Jba;EuRY1_ z%n!F7LT+CEXHxrD8u}+SWoBn${x_-px9;$N<+Oj-U;aC%{WtObkJD;d{;Bl*ThjQe zJiAf4~a05o*{%82PTuzZU@~^zXOp2+~xO`v}3;PBdcX58VAUZg^p!98}2D0ag4^u{EQL|JuQHsB( z6k@A@7%LmfUKhYg^$?ATZ0;Q=ay|1^!6#L(o1D0puj#RSNp!-LuWpo(x3GPulLM&? z`>9G%RxwyY?e8IiW}!aJae4BC)7DlGOGkyUmfBd;>-={VE^Ty8Lb{ofq?M}{_FGu2 zAcGWfS#%O4e;4H!`iOyv!b=c;ebg`Ehhl?VMv!jmnElY0!Hq%Nup|PUB6)`pL|%PX z`i9VNG#mlkV{o$0Z<_EoU?T{oFBGGDwgQ_RQ(xlF9u*}#d{@poH)rj3M_bZ64)6-_ zHm`;4y~8;8M1BY3;EO(d|Khd52Xt=W0lNJ2ZNG=jV=D{Bd2um%v+?*?tNqB-w3@Sg zTpr!-p#kOex4ye8V8Fe=gPzOqELY|}t~2@`-tnanug%bbVr>oG@Ryz4O^f<%nwQRa z4qAuTlypurdvLaSwWrKHt=I5eh(k`jAcw5Z_rowNHU$)N z&B-O$rY3jA_1+*o*8P&W9O{8IBeAJ_U$!#}kPVp30|?ksQ0@+jCd`VS8<0llkoX)m zHKggWmW1Gsq{Cc?F4uO6&IC9hc6wp0BSF^~1Sfw1JN6#3JYD$1`Q{%oRAE1>Od`>4^8I#uo|qTY0)+GKX8-c0&9 zRsHTU`XLCICj!jF8?4YX*rp?`n}#;kuA5F&H!MGDR5N$os?oHGJ@Y(|qf@m!wd|X* zT!}S;t&n;N`EQa2q6s*leuTYODZN$!Yu`2n>N0pk`ztdT*P!tA34C5^P=uGD0b(ED z)FY~q_We~!zN*9E&+=EH&s-Cz?^Qku6k(`d!BD&0KvR1moU32r2V7`eW{i71)c?#^ z9B&gS{FUx>GtT++E#X0$YXNzRZ>;yH zjdw@xsz*0Y`}@-QENVSK_tsg*S%*MQZ7XW6L3RAb{j=*@zVhmTJna{naj`G#hw@YMM@oypz1B#ryqBcV@MP1Jkq>X z^`tvZQDz8G(_K}a{CMyqCf3#Kx)&THaG==Uuw+p5YX}B+5b-Ey3h7U?zkTDa2E+CM zjsA&WV@Y1D{d)PUKeDrLF5X97?Gdu^f`?qs-QV87Q`*-cWZ(FE@K$|1;*ri{@x7v( zBHQ}MIp|lvoX7!AJM!W)t|sq01oGr7V@vu4F2xZ#PbS~Lwh+dh=(P&ecAoeH`0+F1 z6p?XEoc&fSc=BsrL>%lwq6P}j%w4)y?VcBA-RJc#*e8SnOweTPkr(Pu3$)OMVz%s- z3TP5w>#W8^R|t;7S0rB({kj7a462OHVs{C&%17P@vpCO|DGt3m1uXgM;aiqR^r(^B z7sP0y?w4U*t0aL<=LwceCnI|k0P4s+A}bgNBXy>ssg`yImJj&Lw2 z8_b^8$1=_9QduYa4qmxF?si_J#0-gOxq%WNB(9+Tw3#mI)=d)?+3?rNWV;A_F8j#I zn>)#%{POFEBus8%!bSD6 zRaz6Xdh{pi+cqQI@BXRV*JRp1fWIk%KD3y!X?a5%KQCcPrX@ zDj6z@u?NMh)GR#sy%S@m9XHSbsbL8l;HLmtZWqu`Ctu z-hSYDEUy#7mEsuN6(sH~f~oi{DE!Mo_0z#|<#+7Y0T%9fy1GOF+Z}y?|*@1{&i@vmOWpnS-Wz zA!##Vt5O~*HTmGI&giJ`ZZB9z7=ZgOc$TeF!;l1iZGC^us3Dd#N*E&e5(@JOdb7G@ zg`!&zBXuVENb=f;`m-&f^a&K#D+DhjmFmeDmZVA>f8-I=c-9Sh;04_0XFAUblMNx$dwWNEO3&tHasLxzL#X-Oi9(0yX5WQKPap zpHD;uef{a?`M8q(-^W3EBc#P-&g`j2u4fe}MBXVUt?pe@@4D{&I#`#b5vM}OALcEz{DA@aRv*RV4mwi8&L=4ugyaVWDA$?s^_%qpCQ>d0$5Jd3xTxTW5? z*T^cW@^vM*sVa@deA6kJ)cYP>OGPT_@@sfrZN5-2d^@N=nh>Bo56yT)Q_jX@pSbH0 z3Yc*8Gw6O`AhV@rT!TMZn90v39cTRcZD^^R|G*e8Md0HWlRSfH6OLT}rTVQB{4_Pv zu5lSG{9?c^idh`#79Ubcw{B+WUGME$n;In<)29cA+j@FZpRCM4J0)JdSDp_ijiTJ3 zj4^xftzEUZL^j5|b+(0uX#yVncMN#HtANSq26R}A^l$72fnBBPyfZw3o{sL18w>%z ztMrpzeSH4SxADpB_*#Lk5C0J0iYFC@29f5bL!YY`^_y-7|7w8tM3W^=Goc-9QD`!b zWu?CDtG!F>8GHJ>an`XHAW+0Do6mRQ$CPDVI?0C}$J+*9))_sV2^V+iX1x3oS(Sz# zGpD^J@hSd!s%8s8fAe;jlZ6RTqeHw9=5BcB@)pY!WNMjq={+ggEKTFRyUCeX8)CtS z(OdX<`1lqe@@u>I16iG(p8iI&ndPj%@N<9~l#UAaFI{uI3wS48wxx$({|BQ$T)$(I zUn8KWrjg9nwwKP&p4H=N%1l0#jLQep#>3Js`L1+q?HS?_NHi8lS^C=z&gs8#Zf06{ir=`~Ah~ zZ0}#*tZ$(?sd4J(?FaY!@U@Lpl&RwpbT`!jp z))GF)d?BR~7mxcz#nX}RtE$rJRrM@0Tc^-;DwTomSU5{Om5q3&7VL&Wk6$pWjo{#U>i&kT0f8rS1-w>@qdNNxD{ za$eCXHopCNr%oNNWz~r5u{$1i8YXr(GBxt+U;sN0nZvcL8{k#1yZu&X2L_l$Ox<(O z&|zUnxVGyqz;!S4$J1~vt3D3bvhL$>ZP#6ZE6d~bL|n_NkHfXB`#4-%b*C7EHboO;aCaV4`o*WZe3m>oSxBFhRw8f*ENY`e}R(N1l;k)=!IDRs9%D4xiT7a>Sdw1G?R=A@8i8l2fEnYvWUX z?Z$0{68!8k_l_>6C?V=9O-jJ|W~?O(GH9`uEnbir-jeNMSP@x;Z1LObF-9<07-fa9 z`mI8H^_LaG^0x~4@=uF2t1!3zvrlbV8mn_F)H;}f9{yrH^~jI0PsN7RthMjIw%>^b zv=qes`lm7?guTY5k^|jghqaKE$O<_V-yy!Fw&)OdJKi2SxDiThO5=mrlp>4F7HD5f zkvYPwjgqm}ZBMjB6KBN^i6?~CX(@ikrtfgzJGGAU^SoPSpAq2_KTHiKw>*Rgm0{ih z-mv|j+b>+by}Z7?yl?LR^TyKd4B|i?mSeftq_NF9o;LS*7`B8kX=8Too^S3iC)op9 zd(#|N_ztG&3Re7N))S*y?)v9I_U4ZSatC4H$YKlN%)j}*@^aYpx9lyS@BH4fzx0?X z*i*_xk+v>D?zaZnZ0f1IEr54j?9TRP+VZhC`~J$z0?{JCCd!Nh*xO&f|B9{UScmxf z-K?f=Aj!er?~mH6!BWlWfK8g+oE2=>SMd##t&PvsSK$raQm0>C{e%0FDg&G~rhT6M zcxyk{B>rA+`1=dsqiUav=2mKMef4*~_Rq1Fwvg72HjMV;HZ&OFjIVx(a7|S6->k3x z`AbX#@Pn%W^Mi*l{Pi8I%%x#{|1jp&5NS3Bypz*+x0TjnUW9G1$n5)@4SS)@33n!z zh%su9O&Yb=M1vvUHC|3ChQ7A1V+6=8vWzXAGaIck^PFrX7py39ci@M@W&*_zeMU69 zuWSx7A+8F#^9wSrqpSD9U*z3J7bsk|gkLm2{JsC^O$0Z=$l%J~_+kv2qqLvENicMT zyAhkfrqeMY$y{^jco+HR(CuU(o5e6|Qj#e(8U0~nmezcn@>WnZ_n06{8u_!l`Aopg z=6-`o$DDTC<7KnI;N8F6e7hz-RYopxPo|~o?eP{)blh#8_jvfQW~~lZdbs~~j7_wL zig*jSQG_URqpNO&>9-qU$AWdB;GeRr;eUj^H>{?jBaFlird70|fe9A8JAyOKn@Nl+ z6#6YbCo}dPaT$*A#neU1*p1iB+3#eVOt#sQ1&`BR1AO9R6k`#{&MyGTzTM^q@b7yB zhZfj*4+_K@-R|Tv*l4chKG^SW`1YiEkEFROL`#KHB@?mMxc>bLOh}m^SzSqxOs%B` zPCW``zd0#B$gwqBOV~|Ee6=Gp+oNV?&BOuM`Wc}Q6sbA-u%V4Zwf@noqb7wFCtBCH zl~||?NZnDXxGJRL7>zZ$LDY2nox*^_kU0VWyA5G0%=hz89Hq)o8LCyIH9TJ$=jXouqeaD+L}Af?apJYJy$sbW3N4bZ4g^+N{O!Yp)k{#EzV|o zhoO9KrgIdq zW~BFyAR%np|CP*Be`#}X0oaG}P85}f z$LV9O;c2df&F%fo2B%45cp6bhvGn`Qay6!{zPUhhHRHhYJjm zxSMQ^0JkkiWD@(vyeyt6#rWpKOR2#Vts0sUGK2_WgBYI!9RwCWPRW=XJSE3gm@20_ zMto>e`J6}PD~ZN#>t;j z5L(pqc~o9})ok>?g;PHh^IAc+X9ru4NZSCS32Df|ne{iIk;L#9(#W0X*A3CEA=#aRtp6Mw=}ud<8@6hY`z(RP zuCsk|M-bQ~x2^~z$s0EubNuM9mv>VHf`c_IdcTtF&yyDY(fIS}#9q`#TZ9oAC6HG4 zk0y{PVwMre`wzEaL|RN_hujz!=#P+Ck?oZ_h)A%}#ZduMo>_kvZAQ|AZ!ee4`EX&c zu-Tlp6{!VzAQuMbJ}_+^IqjvOiG6qE7~-Z|xdKi}_yS@iWCDBe--o9cRsDt|W2Lfi zGIla}LJp+ePTmYy_vyOpx6m2u&4)nDgfZ+eaH~qYI;#l3(BP$W{OoiLk^_|Os&aCT z20=uaiBia54leIK5!A9igZF_jRbssmum`!!ED9pQTvm4|N0`f(HO%GPFj`AsCOYC% zU@ogyFqbcDn9H}1ftgdTp8|7Py@I)XS;JhueGJTdG>t^hkEV^XdIfX&vWB^Q>#B~g zdd~*Zs$Ye5?zlvFTCnQvJbdoHTyaL4Vc_5LEc9-tW6P#4Z?Sy1HE+BqKJM9+4xM$r zqLe2L7_4plpymurkCPR}uQ@Box`%~2FJgS0t*GxgoyhT}z4#^Y^EB`$W+18pHs!1W zPh%AXnkpcpvI;ys)Nvfy=&1sp#jFBP6Fsb0NK{e8#Vooyhw8Aj8u8IpoB>K6*EmcK zuWO0Jiukzfd!CZlm0&me53=fT!1{d9K`j$rMD>LNPp2#QeWKd3enVCjSsWU&3jNv# z`ibxFkDD=xF%yD-IBCi%w|o!a(~wne^&YkyotuzVKA6KGZNwPj2hvpP7*SKmPKXV~ zT^P!3fiolO63S)u3gz-;jdJTPrUVyi2+GY+!-R6X`Vy4e@@g^4W%UZ>@@0*3>+K^@ zZdYG|a$DRjM!BqBp5&&B+DM9h;rU`EWt_+jn?ft6K!oS63&r5{AO?gU^-}~6aLB_5e^WI8e0;UMZzb-y!5Z+BjS}v4a6&_OHn+e`|KHo9cgb;>nPaq zJEs{5Z4w2cUG)1^ghC<6>C4n&9+!oCm}uc(eHbxjcs~a;X_jHmhg{yjfB6G;(Z79n zclqYw^6L5RWqo`9H~e?~U+-T3@ZrUO1T*`&d?OXSIErziGj@|s^s@bI6mSWXcqLfn1;1Rvqh9pU zcJ>_7NbLj<=W4`HHQo%dN|xSg;G>m66R#`LN*0Hb!b&#J@AmRBx6NBsPmzMQ9NgU~ z9hrtR3$)1GQkkLCTEPLGQKSoTi0x^Sde^k`vDpF0|E?qHcVGNv{Y{JdwGRGp_x1xv zlY-goN0yPA>d2s=aIUSVxXQVpmnq=I$YtRQX&9l)#xmL{S4g65cYfc|@@#u8T83C! zYj5Rf8RTbaIU%3mJ)@-W&pI+?Q<~r%i3l0Ro6sW~8lIsmxK%MxEpP~%;pn`Q3FNe_ zgMH^_GJ#QW2;a@x2}^5DB-HHSr@`sAlB1s9^YgD_RAB|pw$~y<5&=vui%LMey}kbN z=Hmw)T)pNj-dF3_U#%Z5&+i`IfAja3hxd0QgDh7R3GfCzMgEu;pl9R@iK%EfjTdo^ zgbQ;5u9&?E#^~B%C`#uRrtQ3)G<%bHx)II1GJ7Qk(4o2-oGdUUi#JNdIf5R+7l2S?E0Ih zJo+=)ujCGBRwrSHnOLfqZqFtkI7#S~$qHreOtO{ySAdGJr3O@k&7!Ol$>5)$YE_R= zwO&T3)Z!>qn8TJpmD~`r>LjRI)gx4`ml3KoRvCO@tOyZ(98}V!6I5k&H%Sqy@@1fo zB{L|4aiXg*_k$TsV^u;`R(BmosLGeFZSBa*^^um7Ex4uY%P+@QX09|%q}uc>*^w(V zC3!@}t)rz~+p+Aiz0LI%#s112`6@onR#Zo}KV~lugOLTku|Lj0w2h+d=dnU-f1H7) z3bsFv6awX`i5KRgX}$ zUPk7gvGgQ-!VI<<@Bk{>h&fG`6s^_DayLj~DnETFElvIe^Tudl?Y%_OXQ9bJWg1N6+%)P2$ z#?Q#MTFgBItSPkBqSmdkLl`PcQYbfcUEh-!uki)xy0Jow9cG}Zg6X=kLQB`JDi}MA6KX=~ea9py4!7mG9r zwWrvj?)!lC)n*I`K4XUpn;6kXmqv$v1+fgKL^xqzHmDxzCk@$n6AI>VK$ z<2FAVbSyTJOeCq}vif5%F8SG`F>Y61f^mzVEsr2D-cVa4k{>^iVFeRF-HrX zC+5#$GmtCxNj3vtvCs~~x@~HQRRz-yLj^g?2Mjj@t<4T=4qf#yWQR4Y$|}rhhuNn9 zZLx`7I}DE_+F@34oE;|Zu=?Dz!#GL>J3Nq$W{1G%tO)nV@C!y8%2wnW!PkTBRWWpY zM>yl2TpZj2p@9ycnSJ1lH@he!d>nU4tlb5UetXDgQ%<=Ale{ zB#RSJ2_XdR*MEGmUA=!nh`s3=bG?dxsMqFoKVtswx*zywJ9qEF4@??Bx4R8Wi%EM|qP)uj9<71|M-HuyuX?h4Gxkid`_2;mh6mms&g4DfgAI3kWDl+b8pApt zzK)$rZLvX!FwzqlDmZl}LO3w^+k>wE!GRD4-{+!n(Y)x9$sc+kwK#isEpvhbbf};E zvF!hLnpZU}Sqc8N+x@rPt$-UMCPZ;hcOyqv+fD1luH3uMtIM*q_=a`M=|;Lks4yH` z9E0t5gY*s-0>BASZYf}NIib&PZ?DkbRK`_VJFXUD=y&At=s6N614hrnQBkbA*Snok zPNp(4L*Qw!swADphr9fPLH4#_CGxbhntWd_Kw&JMb%+WY&)!tFmU)mTGEOyJwL_?? zEZ8jGJcm_u+T!V6k{o0VP%Mjt)C^K%S@IW-dtII@zX{n}91|To|6jJ=8dN)3i1Rb@ z)W%)ttK%%38Yb{7gcSiIm(xHZQcHGM0 z=c*sy4$x$ARM4ajoue+1vuiYyNtxd(%Vgs9P!|TpMQ47krjgc?&(nBV3X{=cJf4Si z!z5J$I1-W}!fQZ)WG0jcfn1NEBGqEbDsrkIe)uqD&dHP-TQsq47spj{q7$J~t``ri z=c0_S5@Hi7bz}ekKnm-7`$g6>N?)sZb)`f4WKN@CEWJ(%LKyqgIn)?jNkUZSVKoSPRR=jd;XkA62m1J_+uu(vjV$JRx zbV_JqnBNycpGCIvv9`0RMTxMxx6%>{d4K>XChlT`BORJF^bK{uC59pMS2$Wdo(3y2 zKc6~mJWx|wL}P@#{*bVLJU=N(a5v0Ugc??*Mv}S@`V0t6dz7;@nj*R}YbuG7asC|8 z&O)nj#IQR<&rWmX0rJVq#w13gElasaYo$kSiZh(=mBZEztk|J`y;k;LEb zt5Kgk~gNJzO83#IsT<1pxgjS>aft@3F=+trr z=0N1R-3vqF)!Q2q{rR%AaL5O~v;9&+FA%h!&<=y2rsGg*4 z=x>?e=Um0;Si5Cv6p`4SBgXBBiFZNv6Eoi!B3-h~We(7^XVwuFWK%=8El_FABFD~L zDD6`Sh_pOYTPv=PeXs@O5|NfG6bnfOM790v*#u6{)CPyv>#h+D5Jt+`gN77E< z5&VMWTyB&a*s#rf!Vm`bdjIm>`wtWABekxsEB=V>dv<-8sBfWa>0+g8Co(0al}k1{ z&TmqeaqcojsY97X3#r%B%h$JeKaWjdYB5cqvi$7!+C0=-_dQvo(o$+_f-D`DNo!!L z#_m{)+)iu9!blv~YD_T9swveP@oLI%NA#4k$%>%#5)a5=Qx>*1&z^+^-&9U~^#Sfl zXcj|ID<_xd+Ae)A#iRtLg(ye$Y*{T-Om05Dnn*Ef-5tZlb*=7@v-8sAo-5`qTuOa^ zcWS8*)=O-t)eCt}9>Y;VY9dXWbr)M~b+;W_Qv$IhNfeOEaWw8jiJaH$ zg18WJJ}X~C$Qg36#YT51GOZaaXqeNHe#Wwtd}GL^23>DawhNRl=6;K;E#$dvrn?2X zx9{GJrVA^ZY+*;}P@5j}1gIXnQH&amY=fsPr3!Wy$u(rOfizi2y5%gm{99_oxMHp>AFXoN5Xb;AM*`N}G zv@`D>*T#fFri_0Jx5|l~49ZTvTAeAYvA^~S<*t6tWXdEKq6$YbLo7yUL^DbTp8*zf z@{c>o$Y>`la&2)O3$LR7yiJxI7^(|fPh9JGWSij^dus*^ z_SMzjPJkYU+(=_$0tEVMxJdm3Y7e0jy3hvt} zTK_zw4$FJ=sB;=~e$>DF)kl2*`hrp4E}`}Q*Q0e&Ls!XC4uY)a0+>(4?dS$g#+|?~ z!~wJBe-a$h$hYGwXl3hLa&HjnLh!794TA2n*7Y&&Y5y_5#y$=$lO@A{%&)PJ4ekK6 z!da#v22fT2A6d&>h&ok4ZR^_zbcETff7~upA&7Iy!^|%Svp54+4bTop$gOAY=fL-i znav8|YpK8}j{^6-e|iy~ef=rb$sN9!i5WfedR5&|;OZu!==Yt^_eccNPJt*}t|S;E4%7qKYcIu&tlKA5&JAFJcy`2YvaupO~c zD4rWur*L`hp{Z#oTxa;RydRG7*$!e}RmrkOJOUqv%h7v5U#k{66 zwzp7{7&->~>_U_(-dHS|RhSXKhSC(ZQ9>Us>Ra@ipCYAB&+%EHhn-m;I)dz`Xk4rg z^so|$M>9!O?m+y>#Jv_pvvGNl<-k_u$(3)L%#l}Ohz}1q< zES-!6c>Eizj9ifmyeU7?JC;CEK}?*b(^SCay#QiD)8neG?PL{`LZ=+Eik5FFb-qH- z)0|QlDzp^4st2dog$h#c>XQ^%g`lpD>1x@hoGLg8Xn&F-JdP-NS;eU21(<^FmZbo! zK6i><_`Xo~@(IjIuehG|#9WZodWXuuk0Y_b7}IpYn({5gGIr~+3J4#b@D1G6My9b0%^;N7J-K8^>Nn=8)m69tA$$!d;hN|p zuChaFH7g@$7mhz2afwwMMQoc+UB5fhTI@TNQ8kvOs_fKa%P&fdblif3Ld5cJw<|6I zV1j`jOy!zK3^SX)jFw{=A?#eUt=SB9k@K+(`5RkAYd8>HSF{+)@)ohu;YTZ5Ol9pB zQ`Si~Mxo0DuuYy)W@6enpDZYfvG%{&mu+^OCt?uEne89IN5jL(6F|YFpQ^^9)B;1w zG^)D)gnysb{)W)4(=R7TQ<3qEePihb+t3P>)!OV)fwl?$oa zigHMdtdF30*kB2OffHER+vV^s6+inc{X?<(oqxHnM&qG@Y<V4ULMu@U+VF(m=SZA2DEa-!xaC1-9D*sl_WL}3idMrZDp7($j(+i%DCPR0%@TX{ zfkTc%stZukZHf^kGp4wpuE8vMwmQaSA4PR2W|LGWu~a#=xS@J|IEFy3@%{U{c502z z3pztBDMpbf=OvO_jYmytEj;l0gU5ocZ{^_{#RI80g9n(lB%U~sqpuo|n!XYq(I`Z! z$VGSzw!Rr2*dad4(s!(ksQH4ZmQ03-ibLFvm5meC3ZLCL|9Tt}W0Mhys>Wl%WN@%- z5gzNS%7OPQOhzQC8jl6@VMMiPJ}To@h6n2Yqljv3J{(b@W||V2L?m0Vpbkjx1gXLj zAzgnF6Jacid*|k;Ykl=ygSw)$eiSyg_bEr?Peg7=ZE@1^F$fKKu*ht;ga=nv`4XTE zp{MiUNM@r5EuuH30XWLBUCDmE+@VW%aB&=H}&s ziSw#u5zN5U$7T`K$NDt}7A>2h&a8p3&(O7>i&2#e*WfYO zvkM-uYGmN3Y1(u=#Mg+=_5SuiZiIc5lJ*p({T|~Ci5_KUaBi4C+y?D3xW`)xQ@a%I zxt0uWnH5F332qKN9*-Ps@PstIb;iKy2}vYB-O@DNtGEfDYbo*fu%?_8E7l}=;~uvr z{U)4H1TL0lVWoX|T3ES~(QumEZk&Jxr(`VLx;D2K6Nr&jszfZuw8dZ4 z2iO_}8Bv)K5?CP+G6*lj&1tQ)#w>#;+vnU;7(=7xq&ziwbRD$r-wSfs;I?CLd^_VIDL zY|9m~o0={Q?x>AJk3pz0T@B*f%h_mB?TN&brl!mG`~L>ScjO?TsV0&5@H7V1Bp1bvr6kU`kH^!eu&JV2!d}qv z;m^wX_M-?bB0k;W95~0<8AAH4wZn^P)_#l#IhyHvKF@sSPcd}Zxy z33#v_Vb}|YXYj2OS$mMxt19Y7KA=-}YTHD|=dp^yRTYUY*lc?uPJPArJXTRX7B&IH zY!W~-1gJCXL-%6N3`YR2v$u}KglLw&mOR}|8Oa!`HI~WW34>m za)rwY5DGXYMLIL(PW6@nUyxB*=f)2GXvbS zEltDCUBPVpnNNn>EHTsLJ@w%-k9vT6rlrq9pM+`zHlNd{+a+tp2~>p02>x|Mwo62o zi)Q1HqRqhMFu>unR)%5L{^Nbt*z$f=^p29OeswIykR4@-p*X=~F*I*btyP7gPyFOl z84O8?m@>a=7h>4jF+Tyr=8pMSz|i*1dm=eD#!u!*6qeia7VQ@Wc2}o1jmNJ(0Vm`{ zvFwoiW($aP`53f@J|md*E*$C@*RBR9I=$#J+N-4)q#N?6}OR6E#-)&yPU zvC6_Z?L`&=igj%7AhRI~kKC!aV2A{$_9PL#5kIS+4t-9$DDn(_Rx6e{yfWp83teoN z)Jvt2Mxp|(l~BOtHGB~^CgCZHA76iJSK^MU&bb<;CF1~RLqbe4VQ}H7svHxPu;VY^ zK4SlYZxezotGj*9qQKW3!Qtaz4c{(R(+#X)^`(KYJCY+!fHipgIIsrQ7lTFM?F3kZ zw+7a<`;f?6)`})^H>kcCEaxpQX=^g%*Y?VR__*0)h}GOCGK0IU?r3*z6^gYlT7_KO z;#RR~OHQ%1oG|DV9^s{7!uw4SyJkFmVwkXpXBEMB1xxX9Oeko7D7745vh^=!m{6)k zuI(c0J(d=JIJP*BQtk+9`y;~;&!XhdE=)1X(`Iu`0y^VKB}(1`AKZ{WRH)cCo^bsv zL8%H8E)tk{g`&y(XQ?jj_y<%6Gn>T63|EbqyWM%ocB8e6ilkD`%hLoHiG&=f4e+Q* zO`21U$Eic~hU5L?O6#glg!2iLk?VEYWJLNJ;8D?6HXo3hse>GcT876+n6RxA1waMP z^(jl=SQ&DzB&sEof$0L5q_-YTO-5^DG(uD}D`R9b5>eH7ESQX=h-v{IBVodyb21W9 z)p#tJ4xCcotGdn|>`c=-*`Ux|%3aXbA ztl_D(V@>2&BWL87B~WY#eB)^mxv5EdRwgq01c9IXTQY%fl6#mFERQ-jA9TwL&DBQU zkmI)sP^&qm-Q!USt=iU*t)Ii z3S&{{;3S$eYlzu*LJzGR6lTpz6)fRH>>(z62%ahL2X;MVw;|7>iv_1J1BFLdu}+Lp z0c9qjfW(*yDB?iE2pNCE_dF%DPm5Hd>v*Wu>Jq^N*KKAzmJ{$dO`|N+X+e)~Z?9k7 zz5C(r`zwygf3@a-`&Wreg-jb{R8!OOT%ED+*{0XwpBHa_`sRy=yZaAce6@b_`kOC) z?vx@uZgRqG(F@1W52cWBy?A}s{V6W&xw_>@{%ljI$!;ty$HR83I}59m`CMb9Q~1tZ zHwn_t#x%Cbari!KFPuG$Je6Pte$1;KY0{Clre{Fj`O|6g2oZBD=E3U*g04Y=OCEhY>RdCems^L9P(a zgL?KVeVH>)CT#1r3U^xWdtKZni)-yPzc`$-!VrCcFDm6^8| zCjcU4lO3Aqj%2$cu~_|JDS-G$DS$YVidBS6eB=!>HBqC?{(%%gWDgR}B;6CG7N~ZF ze`t%LEZCME*A?Jl3%vt7i2!Sou(u|2R1<@~_< zz4OL0G}vfpe0KGozq4&+A?=R1x^^LP6>*lFy=i)pvOWLcUV(C=XYs-1BK)TaMN?`C zkC{?F)j6<6=3dCA<#N&4mL(`c@keS!dCgV6wT%(hbZaf}ELbni98BKB^BFwhvKXo!{JeE#Iyc~I)s8+*_CgXTK zZ8AhEGf5am%wZDEb`}kR<{5;^s`$uZg{C>oDvA)b8qKQ3_3xHhbtI2MQNgTQp~6Tm zg#?%0>~gp|Io^3k^VioOG$;F%iM#-1NXB3>KKGq1javfvOC5RjV2tB za0eTA2p}pPd2bwE2)#{z=wsWpYQkugV_K~KG=z)B)Cqnry;E=;0}}-$2^NU8>uXLh zq5&viBiocdQ3GhBS1<*S-cpiD^5&B;L?L)IhJ#qu@d^AS44qoQ|6N#dJ%Ld6N{eU0 z=xh1`YMkFdO^BrGik6^07TEeAu?|~OSaYlyCEXzS(Q^1Y~7c4 z(3+Dw&fh)`tO{1I`eO16$TTK%)cXkhSXHj$cT{M zZFPRO`E;r2+f=tQO zcdOXcO(-HDoobyOvWeANMM{>2qrKNj4tiPP^-^Mf2q!=kOKuX#vFxE)7644GZ2-u3 zrJo+Q8<2{yD{Fu85L$`&qPE2k@M|$m+2bqIM{G+83XDz2Cco>rm(D#AWNuGn!*@nx z`_i5mcC^c)=9n55b%0qTGC9y}qg32ZOOiV!2V?(~7sp-VX!dtCrc>g1t&?l4E>`k2 z9o+Zdx_zksHWmAQtiI)>YOCG4SXaoGW8}uOF}hljQ=@D94B%v@7z_PByPY=Mf;9G! zx^8bj*80dia#0eOHJ`idwie+u;4F8ujNeE|A`QI7_oDbQ;t$r$KR z_X2`xjXLXml<$nu`l5u%b&U5I=ufBh0q6@xJ*GyU8uh+%O^t#6gi+U!KFRdXRYhp! z(wUasPJ46}w6ZnB&g7ooPKy{zriAiiXKvTW%>dH!WA1zMH@!xicDvaj#cV zb4F>Bc7rC^alj3=*{-=^CaycXROPIF(3vngj$UhJI!%XPW^g~-bM-2k*tye15|wS% z`9wqn<^hg3lJ$~!ztAYEdb`Q|g=^4&zyUA@MDvvZM}v=j_@*4#`3r~7%&4!kQ5T<> z9d%$9yv}2R+9c>1I88{V&jMJa3s&xq5`>I?p!-SS8@$Jl zJh#^Q4TYzlY58d5&iQd?D4UXxl~-dFbclTc=!I6|Nq+!(wt&Y*eIKImr$8?~7uDE( zmQgqNQa+KQ`kT5R+1>MDYWqN|IyX-tiTrg*l~)kG%4O(_p+p(89RP%YV)v0e|y~#D(y&R^qtB=dz9(*1{-$XUM%C@`g4ah zF|X^(iuM$WfR3fI2urgpwHHuJ&7n7j!T{gE!U}0`Dobjt6p)7;|Nn3s>|RMuxSPXG z@X7AIWeB)W4&tz}pbU`{%Z{Xa#GtM+UzbUbW!&8wTRfub@66qh)%In1i_9G<@yZrc zS-ZvZHrNPA0N5t)|3kOIZnQ)4T7uIu-MNB^D_nnRdpFNS!E%MFQ#ckj^{?%w6g0jK zrvfH!gH=fTdAHTsErP>w<6Ji1vfEPN`h}43{C3#CaJJT|_byaC-Ve*JYWjZI<>BGF z-WZF-mMEeFE)y|ZE%&kTaCZpTHZ>%WAcM_rt2;KV07euFI#DDXj7VcdfjzMVuy+>q zXEv$Aa_)v$t`$17*W-SnLzjn#n;%T>w!zy);o*!(=k8J&Yqxys_LxUiJ4}MpdTY*- zG%$$JIF9XsWhmzd7GN~=Zl}jB+irXog@b4c$W77IZlkt57eJKrB8{V1z{}_?&P$EQsEQSgbL8uQQGtgiwQ)R#5y=yH0QM9f zpgA!amKya5lW`0YB;!a7uOys6;!`K1#$)MZpvcS-)iCME1RhVDjFIise6|EB>D*io z3go%;41(2_J&r_VyNbd++N#NuAnDe(!^6u8X0Hwv+BWM1S~5e=0)s-2J)$Gl!?1{t zBjMpCP}(s?n4(EK?V4;$(Kn7vag;qX~^p}+L*`ZHZnJr7c%Ngpz!?|ONa=6Ushfa1!$3I%f$hGeXcN7iF zzz>6H^hQ#L(A)Hf;{2Ek$h}Uuwpjn5F|?G_*wlv#fu}KDamS`i9{H*T{?@=L+X|ZnB9)lJt zU^Q#7kKSCdGbYbE-ezK|Oyj8d_+YAPw2%gwCd)RE9Sa<-o~k9hroL`LsjRBtE7*54mFjXxEh%0(-Q4O=y{o4HRQCPJW)oDrY zy~^V=HBp~Jf7N!y+--|a38Al}NNakCv-awV+phRWF4G-7C6p?=EsIxc?20j|=O{Ef z6(CgyA4qk1c3GL5!Nd*ZHbKXCm1Eh!>7#uE-vq}DULvP^W|d>vSY_cXun(pLZ_mib z*mEco*wy`B3GB3qs)qo`bw8?y=?57 z6k4<)6%Gb^gogGeJF{+uEl$;H>t!66)uRU{2|aSb+Mp(xXS>%-HK|1c>xF+SNryKx zsE_0N|GXviF^G^I6UftsIY%T2x*C^SY6o1Rb62)DZe^dxr80r0*c=6&oBH?|T*xij zz{N$K+bgzbkVleiH6mKcwMvEh5jJJ3DRWz$8e<9cVelQQP3WD*8)jk(fsqV&M-+{K zKr>Avyv-uKlJ;)g`|}LH--%Q*%aB?sv+#;%BFl_NnFK>{G$IAWi(S2BU<>RN{n%Jy zq*l+n-S>&p&$S%6I$eNfe4MK%h?em8qgmYbwX^pH_+yN!I9F?6L*!qBnonE$j>@$0 z80XDwoF+x>uF`kn*ZgXYN0Q8%VWq}Fboo0wu10a{-#Gu$nhz&cb5H4LQ)$(qVi4zv z>uq=r)RMxv?9F9L(FBsa_`5m`$<8=$GJ;0l;eWmTas_Q%aa2{#QQtVDVuNn^N>k$r zNY?0y^nNd8SDDd&XKo}8mxh(!5mbB8Crk!x_2qITh=dK#Q9 zN9G(wRXIc#P?W~WWsAlzlv9+gkQPO;bTjraWIdj$$5~H7QD0>+W@G^Ao`bz$8Rf>L zf-hg+zP!17c=PU?XYW4VIX*Wa585xU)E?sD$1>n?{o}{GcfWjp^~JwfBINZq->iS5 zWX1pT=JmrbzZuI^Og}`FO^#RH&+WT6Z@<5~dGqk`)$8}){Mq;S#b3tP9ytWF9zJF1 z#g-CrO{_d+6@Z9+f=u$uG>>0HVX@ifLgd7Ai+wL#h_USi@|hHd1@}(t(1Ywwi-(im zj_xIj6=>DzEH@E}r@anvvoF;nHrNf0uR6~Ph8E?6)S-xBd<`3e?!X34iDihY;KLJp zwtmmIx+*)Au{9f%q22VlX_te_@jFUr?Tl-$=lCUY8o-rGNR83UkC~NmuI?-YsoDxP zTV;gaaNvStBeBN=_=5c^!R1RhycJRiQ> zW#_}wLx0`jALO$h!NEBQqBD;Wv3m-wXhz*@?nOLu5u>KFT_B6luIF3`32|K0I&N=t zYlp(=c@;};5}dK@p;;E;3XK7gxo=z8~pU3#&qMfWm$4ogwd$ZX0VNAv6owNC_5e(C0)jwzB?gmmq|(0@x73hM6e2*=2l6 zD2z98BG+xNrFVVWoR2!SF?hH1^rAVitx?gdrTDFg6RrCLFagXpREGy?OFb zD8Tg|i$F<6V+||@ziuGlzdh(5qygDR+9X8ae|w$ARWM6J6HN>d7$KpFCj3GM1!CE4 z`NoCpNoY%G#p7o{j3lmtMiXHO2Plk?P@{?EBt(vmnT1TR!B8E46+|@A#9|bNP=rxf zKu8#Qrx)@(A&oaNKw*rK#+#VuMO&opV;6EsMs)*W_y*vQ5mDWMBO&Dbs*un@oO^+v zl-Fi5w=N@1mY zyFHeLy2c_`OBoSY8t%!jrL^Yu2~o92;{XlMi=YHm$JgZZ%=a%fvHWp!$Yy3N&` zA5{G|9Mr1O3_eOpmLO!*9M#qkdJ*hXc8Qy1mqRF!u0b{}OrzZ$df1IEY+M@iMsjJ@DM*kYWzy*f4tuTvmUrk~4uHvggR^bGsLbO~E<~eM!x$Kl1e~a&p zObbaEwdZ@;`Vt2`wfssW@e0O3G_aj;8n|j{%o^J`!4-!VbLGhB zi0#jd=O z(Lf_rip^2MSVlP(*kgITG!eAN(wKGAt|d3RW(Bj6hSht?E36B}5v^{%P!VUj++bme zK!w1My#j4sm*>Y$Pbiaq_dA?i@V-ghu9sgUi1uov%qmXlZgXyfTLeM`Py`lU0imA! zYfTyC2L;7k~ z{y6s-Lh;IaYD^Z;x}s&Mw*I&F@HtwaiuD`BQ#k6d^})u@S5uJI1^1pkV>?Dplprn;vi_aMY6-0^{7XR?7(n-e9xt;7BW3|l7$vH$(`#Rrm&~x z7f0`>xEOGMQ`(Be9e~SnAA%M13f}el$h_`~)hGW{6}VKiy9dR9<2qPOEVGX~+(H1w zevcL@XSA-118D4~4ZcLqcv(H;3#9}%%nAN83@73A1YMoVYy zD^V;t9m7QT+DhW5TZ*%wasG(q)00z99mJOAtF$r86EO^L_B#AUtM5epacT*;D;tkx zw}krbE#VU7mA;y&Z$NOL?j8U?A|7PYiyChrk&ZlZpK|{Mhspnqt_^V@97X&hZvgg? zLVUp8+A21BOw-AuZxpd>toijC-M@eV^g6BOD2CA9oTwo}RyUq>+aI@HO9;lyKLWYW z0Ti|MG}r!ycR>gDfka2^B5JvGji|22cq9*vGzbGV_4KMx(7DlVH1@%f z8S(52O=R{yo2Id&;d6W#gk%!W#KYyztg35M+eFyV$ByM0wde~h6cQgScr0{Li_bey zda>hDO^`XafCxyNw6zj8;3BR?laQ6E6?pCkx$vcIbW&&e>2maT%|S@$vr-7AkysVE zdq$am!Pn*Qp?qjwCzw0JVseuaC0?YgI|X^JLF*BETIFja%h}u*S8zM+xVLS*KiFDsd^+IRGM>)80Y#eCV9MuM zeaw-;Y3}b^gVGK2gI346kgXu!3nDF=^DJ>rSdXvU zSlQXolJ^$R`W(y=b!t9|mmlN&R?0LTt2Pxi+ch)QfB|=mEPh3H23Q&>+aNR|_hEco zd3*7){S=1R z_gdOsPldd-W*lGD$f4i7v%Qan2yS(^#eO(vOM-35(AG}dJ?-qYVkGMp$=U_!V_f<4 z*tSMzM6~^I$ZgCkkfD_g*_3ELl&Dd`J;=Rg)D zh66|M@62x-Ur#}Pt9TKy4)&28lS|Z+x*p2ayW@MMkutb#Tv~8jPaT989SOn{K^eUs zyr;^jG=T~|!}9d%J71AVAuwk@NMkY`a4MU|tz4HRmdqe~sXPm9sAbd;T@+@FlcI04 zJMMx*cyXj2SPg2xS^YXO!xx9V)@N-?Vkt2s&r4eHR{?1s$1@IIX&3yrFG3D%7aZPj>InndP)Y zkUv!@etsKz6Nao;(M-Ut*wd~9?9)Bse}@vH4wfa1t@fa%DFL^=lZ6A?&(zY+IUBzrg+`0s^0Zx z^GS^g2G5g&;P$eSN)%GoG#yjinL1*V|006c?$-pxphq?HELzzd0C7 zG4L!ZJmNEd=b`dn{cQN@b0uV?B4yM^i9UO>1kb(&)|dsbmlK&j@*@9SWR7Gexika( zsk+71<+Fpr=Y`6u%`!p@s?v=`O+tf+>8lVrYT3}DZ*&bYl|~YaH=L)R8lq1mb+-qN zO#*3Gv!Q)&cQ(3<=8w1mdms4)2aQ>0>McgFc2I6uWg;xbV zEV5yh%d|!i;oz7qiYTVCXWS!;?0SVBEn`iNrrT{hX1i^%@$^fDo=p<0CJLX?E+eLj zr9U9V#jMJY5HXBmlzY);xCIXUYISFRc2Do>(aip|mhz0=jp|^3RoN+EGN*@P7(~?| zxz6A4Wu%8w=E@d5O{t!at?)p@`kSo#i5ym9oF)N3Fs~SPPNW(Ny-9xmHog1nh5o7d zB0k?@qDXR4&y-|G+~VQHa0GprBs*2f8ppinprO3!mU9^+=`!>>qd-@w_D=ZyfOD+R z2ZPvZhUu+%fwD3$^BY@+$=XG>@Gw&l zx{yY5t9A$etY(fwrr9bKi^oLH_{2^=ODK~*Buu7Ncv~S1gqt=6uZ}0{u%Ru7s4PO9 zPw`3xA}ND9?yH1c$hf0uth*yKZZxD)bkMTOi#1y}?eqbRHtEee@k#p~1}>XgT}daE zF=B^FfsQvmVJ}#3aH&b>j_cHoD?7hH7e>HCfdfu>8Cp0_)c!P}(%WG9I7-6ds99{ctq**S5+ zHiP&=mH5d$D__2(Mv}*Ru=d#(2=p;WwG?jSLSEfc9_c}XFgY7oyD}8i9n0TYRIFNc zWk@8$^Aw2g&Lz?4?Y?~P4H|l?N(*z~@?i3g4=Nz~oJtE5oJEUNELmm208h$b!qQT& zGqXE3lg7c~GcBLAJ%xQiv1MgUc&={CHeEuv9lLY-{KENS?Z|b=8(sQeRhOm#T~XxLu?PvxGYB3A~|@9i-YY@^&2TqS#n^tr8xcJH`G}cYiZ2C!H&!`L(_7t0m_3CAC@Qf{Sf1o0Me4Q zbp+1xgKdp>Z9~Cg{aJg@H61*h(yLBIP((rODW`u_o(eRxZcsu*Mt*>S1&7z;*-r0u zRNARPbg6ZdHj_xJ87d~CBW<-kwuA{4)Op@Be4sXQFbsC<`ea*BNf4CHODbib7ap%B z@_1*CKq>xVbz0X|i9bucJb!ukS>t&hU za~Nk|w`BP-cWy8i(LHgaevb2rq=1Gb-nZS9XZQTvd5e^z_nP*&rgaXMZZSsI7m|wQ zcxYIo@GvNDO%&LQc?%R&TF2F+y02)rX|yMd#)P~H1iiTv?;?ZCQI5P#0>zVJ6tY+`g7b4G&nbh4hS})4AxMP*tm*=m{EJf6N`WYDlx^%XIvQm9q~wfd=j80LFxfAS13XGL3^ATql{ixN{P$ zDy08RrLkm;G)_SA8?d6C%xEYTyhlmscNw5hzU`H`hv*7nxN%322^=glc-a4)Cy zj;IHgW94pX*d0+lVS>+S(5j_ZKZ#?JKy<Jf&MDn})t49jHbB8Q| zJ{E6@qyjHDCS3+P9eCEzO^BK?tVR^r9DKo*F+Ayrs4_yVnM4%7JzLR0fh39@zW}cu zX-luk=7LY~-)!oa--?Hr(+|GJ>WmTCY)sJFoDzSEj1G?BI}_>IZDGIhCo7~mmhaO~ z#~Bh7PF<+G>n?av8OI}m44>*JL_U)w&*3buq~KchYw##6V|wsIuiD^dNqxoXxWgKt zfzvzU5x(wKzp2S*>ZLW9H11iEVF(4%F2V*5LfUd>AV2`=ADK>yyUZ0_Zwzmw1s2J9 zy9=~kwI}=?hcE@!>e*CRqd8P&QrO*13!C)I?X_2R!)g7@760LAj))ftOG?;{=!IvI z-e$(ki{xYsEI7)7k;TRyPzIG6LeltAm6E4oe%n_>nXjz3(JyF>$)>7BY#%P#*jh2? zF^fMaXlR<~>+WN9%EO$TRTTv0OY7yLbK#86+;f`dJKpL*Xn4^Tf27|s`4q<9ltNNY z6~AM|^8H-JASHES>nlYT7brqS`lyV8mI%Cm9M6=w`X-n^x`_k=X(h@%C-9v`CAuUV z$lw>Ly>?!FlSotF;8%T&ZLt+0PkF#p~P`+tXbn)MH! z=6?`R|J+RbFU-?0|I9oMV7B_jJk9otZ23=a>6 z$+0S?9Vz(sQ8A$iDnGU10JgNWRIscgh2#^Aq-{TFU0$kMekn=K6JntiE|{OP5$Yyg zY!Vl-6h0^KqIzu@*E$_iM&Dw#Pj~DV|Lg9`_QSwUdk28>P|CLwDV^78XDt16VvO5q zCwg~>Kp+lT!x(D8h9g^k9zcwZgOibvkdU4pfdGeyczbg*F##YDBuqv@Io~kw<8iV(SvV$|p+uz^E#>O^$RSKZQN=;6Fy5G%ZNvETwg>?}T78WI$7Kn~h zrHP4&F(uQal9iXgkxYI69t#_LKLL)7({ANV5>RJ50RqgZX5oVkmYHxq64i!bMCr6e zR!&Y%x*r84<+vH^b~mn*ON@B$MX!fRNGOrNR)v`za-puSE+XNZE6JiSR{S&m>uY|m z{ZJ-FV6+ByH5vtzDPT0cV4cx83rj9wrs)Rh#;u*MSwdQ7_9G%XI7`>T?5(3%8orh-ePOrzfxOS zTL)o-$}Sv|F644^b9Yr&RVkFH3a15#xVhaffsfndv^$b78!@}Dfoq_hpP#o~^MZAG zhlzs@eS_ViMc`T8!?h3I^g9Lj&rV8A?7^M9wq=x)gFjxOUaFVrBQUQ#EJoeE2K;t? zd49f1?Uw60W8;eqU$;m0{%udsLqWkqGxwuiOqJaqxAWetDOX=ZSNAxZ_x6*lnVGY)CBe>1oWOvBY_w9;N|DD_M1mC4 z)xCG4##l*7p`Dk68BR=a>vF;Ta!UPTxqu4|9Y4SAXln3yrUEor;>AVi$_h?mjx1G5 z3`n=jfT6<766(`gFeXk$dqV>TB%#B+aajLANyovbnhYrDey8X=;MMOj#$I_p?vk1HKl2V4&J@E_u@o{KK3vzG1Kuz4S@~M#>44bfU8fFF9bMs-d9){47mP6 zq9%{eTGx~c6#LgV?US3I*U|O8`}I-D+vEG7KUxHZRR(V8wz8ag&0G6It zT0cELcJ`8Khm#``7I~)?6eJ{lHMN`Tc_m`v8ZbE%lNLb-1`ZCd8kptIr!&!tGW9AW zSVXe?_eeoPx=)J@?N83tt|KGs<_=Wv7UU`A`$B@LczE&kgw3rtBZ>>f4%WwSx#rIJvh*6E7%{Fnoz-#1*%nEEKsIjTv9P5nzcFW(_x^ZqGDtW z16EKYL(w_6u|I`@(>a^vsd4ndCnS91%O_6Mw7t8_g)A~JOL1W^ANpw+Qy)!DZ8I1| zM9FVmuuhG4djy7CSC>B^pthkQ79O`xB93QS-mWY@9c!CJNljIip=O!nQWq7_*^++dgMf1FE@JkhliQDLiRq2z1VjanwT2K zR>x497<||2gHV{)8WIwO3dBBHZIxJ%iSzD^=$d2fltqFv;pP^T2hpgItw~sJ2*KyY z2qK562dp-l@2mP~>fax84h{@BNTcB4Wu>NuPQxMs9f2`8E|r$0xVX3t(QrwXmspbM z?+SH&=@(td@C{Ju;7-G#}~C}UJhEAP}Zp>O#0<)gJh z*GAybFf{^<^sL2^^;TbBxcF?d$LArF8Ka_66R|Xg!o>1;RxU0BmPO{imE3GooH3IM zi4+ZHFH1+qti?`xj9tPvaD+{(2O<_vb7(Hw$9r~CQuOCDQx2G@OBUWu_POq}6vR>q zY3dE{w(6^^%T2Y2heKB=+^cfIIBy<&ZcnOj*AVCTS3a;!!oviruGocS6BzdO^@vwWgS=Tw>IF3dCL=KAOcONV1+58x&(VFTrMadBbl zWX=+o2pb&41}gsQDz{vzBjxiw#b7=#Wo#piGL#%y5t7N{QDp9*> zv7Ohc4yStWj6cxuwt}Fhml4!xjXRh1MGB1|Drnpl78X{YAya{5KT1x-IS8MRGQQ&R zTn(ZsWO-dkO+yksAtS|0ZvxRLC967a2-oVJ;`s{Bl!t-|_<(uk6%|1+AX~UD$cI<- zD}ltFbg|bso7+xXc=|nGhiW`co~#nahWa3Yh1(l%-vY81z)T%*nSz?%Xu-UlCOcc zvXnkPj@Z-7?KLme?M~U&hk9Jwkd`{MFr_F!=vLDg*h6)^=kS?NO?^I>KXcuV(oW!9 z^KD$3dqZ6v(u+ss3sRFmSt1_{8b@jkesRt0yPQWTl%cWmA|&lJ0%>Jw@9gbe zMBu&5#-r{Pu>>oBNO=TY-UK2vl|YKp{*AyrA_CD{wG@&}A%e`!BzRI2Yq&cMnsHu1 z9`KlrKRrf2ugC4F>j#qZ5)~UH6uPgD7=Ya#!Kw10<*hk_+Z*MHnd((ros)e4ks3i8A2YeW)2_8|;Ep?r< zGNDg^eHuVQ2+hvlVyeAug}MlE%f9u%^LmbDZJL{d2`K=X*H%LZiBrP@yi=(`z4)#l zm<0rg?Ih$CTI1G;JSzA5A~&EJrz*5ZE*y2{byK^d-(H}*Z$-f3p9QQFU-4eyIK>Hu zz70tarRq020c77((FDVSy@{C;YD9ul$UCI0iyr#1!1XWDO^T z5y?2|ezpsqno3c=)oUby+p8*+0V?=8&D0QWt z1hB%aA$adW#mC73+eL1epFnv91)v+lg9#>^15rNA^6hw zGrHQK;NbfF%@LLcSGv1tv5XXv&su6h-UHtC3vmWI#A;J1{zCzLckR@0o#Ek7aK%r9oev4|vkx>_Y0g=uMLM zAH28559sa|vhm%}&^W}`B)<@edVac`r1Pu&Ldo)q$P;-G_OH`1KS#(vM$G^w@jr>l zevDoJHk4-l#k$D&YjWnl8A@|={$nJ~0T}Q8-;Jc%|Cqq|gS+wnY$Tnvrl2Z;HfSzG z|Af!S;bVa+N%)|CXE&oxm`U^k+`&E|T2{1Q^)a#wMNr4D59Z6<5>Dx+y`Z(&6LMXM zZmMpoDQ4R7?u230{OD>s>%7=XYUVi8?Fl!ld56n=U&85LoppJ)iT4*0|D?Jhjl^4$me$6LqgZap!F18YP8qSOV{u4zMm^msSCx(KakzKxTqbjiySGO zwG0Pbu$*9FW(KZF8HhAcdjo?}#(-q$;J}n34IUd(*^j%@;sPrz5)Ot+TN5JokdgiD zHDCD>8=DgwdsnIZZe(KOE=~U?S^AFrH4>wnSInS^x(g-R!zHnBm8i%m+=9`yk zVQLD^pix<}oLv9cuc9?vDh#~L{TJ9qJ@?PQTU`1|5#b35G} zMVJ^>Z}P_KI|6A~yM49bXp5)y?jSfm2Wl|#8qBgi90m&7K*kZXY@)%DDV!xtF@KA9 z%%Gxbp%~paq}An|Y}?6)a&=UJt@0G+`|~gbp|MeYD>MdSuOS^8&aNosM775ufPyyh z7@A`9Bn=h`DU4QAcya^Qnftvd^2P5>X~Q*qdeE<2U^HydOHQ=NP{0`y2n-`=)dXIj z4jUTWy&ji#x8nSsu4YjQed@yCk$T7Dp)v)KH4YFG#+2UYbJKpK%!RXJ`+5_S{Gh$| zVump7x1{?>By79DuRXW^WQU*xmJ;4oE2Kr1sM_dBC^~Ucpe&`UWq}M_J?I`&TCJK! z(-a~YZsHFL3Yx~Cy9pM|TE4$;nTi-OeD3a8bEy!LxU0OYW-){@AI=t_jrq_o=9s<` zMo941N{SL^4>8yBI50GY#-hnR@huHzRAELYVtqZno&71`U$0wz{k89nrk}R48ZQ%IWSW^!ad7-+mJ}DlL!vxBE|9A!$H#bCh_-+*982C);T#Sf(V9XTt z(4vYsO0h!Elnf;+F;uQTdivx(rLz0BRc!qQ9+W)E04*|SI9A74jA7l^E!H~4aF~#> zi`rKWGK2UWiI~raSf9kUulELM_0eFEiMb`734nUaSoSxZ2_oTVNFW2XraKTxR@ier zw8JC&+bS?@Pbxoj%}k&7o~#DxdryB^Tm($cwR&-yK6M4WC`atOn)4wQymsWH!^jhq z&MiEis?^d{3>ba4UB)AnN&fU@gunAf2#jBA8;?<|<*>G*^#&{6vOYFcEnqJrH2gZF zy4eIGp-=2(k7o{(J*(9hpG3`}&f*IeiK6uKLqSRk#HAQ|c;8ex5sHX*bx61yv5k7& z_^OLHJ_$3M6Fo>F)CrF@tWx_XNkmLoSQs~0^SiBq-Y`TF1wp8-q+#wI$rAlW+tEWf z64WTu%#5Kn_xi1}{CrkQ1q5;w%6mm@9G=yshZto=#~I)pZ?pPAg6Tphkb-2upQ?1p zY086&1o#S1F5(;iH$FSEMh5;nD2~$%DU=F1@r?0k@oElI{p~4LZHPWDYql|{ND!^& zlR1C@3o;P|iFo#sPop(W@@^hZbWEZ)l<<1$j3YV&kr)X~H$!}=yzbLSY$xeA^5;JVXvE4#~*gRqpPE15WON{%6K68+9b%SM~dr_1Qskh z>P1igeeYA^hiAXldS39PBu=ORn&x8JLJI^$;E{+*mws`R+8~R!1D~%oJ~54lruR3zs9P^~^D zTXnX<4jgO}_=X>W&XZ&9m{)`E?q8Fl16-D+r&@WGmoPf*h~RFDuP zA$ayk{DM?&G4&&#&%;#&D&@dHWhU4%Zjl%YWCpI52|dia`TMrZ4?u->4RvRU(8xWq zbPZE0(eALzjc%iHn%8BUhx9hb!=XaK#zO8+;d#;PmE?lp0i{vC;02xT>A|*~fwZxe zS1%?Wk2f81fs$}BQxg~@e0(C+ENrPV%xn*i{>lfb%7fQZzs1X_vvt9bgzxHS5shlf-W{T(K9i);Ja3Z@v9v^tvEvKPKlXSr-}#qYn8SRm^fT(gs%`-YG`r6FpJ`7I0y3a0pZ8*{O8$@zOV^5@#k-|}C!|C9Xp?@LL4E{VY~ikiAw z8kYSdtNm>(@c*4E;1U0$D$Y2VEG5e7{#6M- zrTeLbf6&nXTx|Ti5`HXM16E)Ef1;R=>4DZpu>=BI7f4GnHMLu=)J(5GOijTK4-a(D zBe`W-VZ!Tm81@NTG0-%#eg2T)VTNON*OBg#dz*3ewA4qvc%Pi*#zqd=#z3g6kLhRZ zBdQ*4M->WUPZN3-vmtl4A4BZI)Xw}~4W_*i&%qACPUzdWBd5}bMj>-mpRS4*ICFcm zGB-%~R=2Rn6IVp7)z-3;7hi?dkgv|C%;xO6LlfmitgXErowLX#$1C%$J3z}$S|_VF zD|U~F5w}Z-T-t~?U9T(lQcn(@*JtbVtQ&s#SqUziuV2}R!$@`!Gmk#(@VOhmd^)#y zdbfqV-SaL;e@(++9`ovI@PX%5>iN8nlF~&`ex!^2+DPHA;~u_54(!Cyfl4{4cqU2Wa1;QB;?d-2{X02&hHlW zo=(+OXVNSZj{ND{oa6PMPw3iE+BWH`bwkiyef#@;k4KzZE~|*wnl7_)o^TcHF0>U8 zrZT;F-+jr%&D6bC_E~&;;?_Y|C-7)@lp!E9UVcvr!D4N^2FW5^Gv-waH#$^ZH)WtD zo&q^bB4X*W=d8PVRw8U7jZ9%L`cX|>SsIV4q;E0PJrNJRDw1MfR9Q^n6VwYmNp~!H z!+N7Jv@2f+d{yiqH|jg~PZY@J18mS%EJeVpQUgFLHTZV6G(~dA{>8zmUbo5q@&L@v zo}QKiMa$sIq-coQxzB+R24-$AqGJ;RPr)ejC9>`tN2+5bVi24+G-rgEy~+nshCY_w zlKLP5)FDHr389YRdeAsvxU@p9>=wanZ-a_x1}Y6HB%R>5s*KHE{0kA5pfo77aDltb zx}_gCN5Aw$k-LS5EAmC5Ip-sKHii%!$tbW^{@gHUg3^&tU?Z7(m9>KfQry0YCXvw6> zE0Ky~nn0edNIRB{lzl(`1*X<+@EFz_3a!o~I{ zOyw{0B7dS(oQN3ZOie5eh3(ym{*F2kv3CLB0|3AR8A}rLfwuqs#p$*`-4~&Wc_=ywIFW?HJnyI6crM(>yBQpcrU+S~`W|;ro2cw3j7N9e( zp8@h$lqE{s%3+u)U*+sUtv+A9FHHKM*j#ZuKX)1^|8d z4Xlv0w6i8+RQ`cO(dK01U|?YiO&`d97#2SNXi zabf>);va%K{UNENle3G7rTtIY+#KkQ?SFz&7+oA}>||kT>dfF^V)my_e&e%#;F$nA`3+O=bXat}T2SYm(Lnj6Yk00VP*gKm4X>Nem{ne*Ae$OlZ z4@6^SVr2kW`$vPUT+9sYtN^h%7`T4O`&Wnl*~M=M`>&#tf0gQg)1g`Z z*?0hV{F$QsPaK+qmHU^q{t%0Zo1OCy^Zh$Pe}|PHUh^LW{X6)M<9F2i`Q-jTacGv` zUHo!rj{l2D_7AK7)(IOE69YFBK+VkmPA7jg#(&Vs-=i7FZ#VrN&Hj^1|M1~|b}(_k z}cuWZ0`ue z@kcOJwSQ}8X>1QD!SM&?P23U?a6~K&9f<&#OGa74za0Do(*1bNaRRjd*SFixy1(!9 z4{#n}W#Cr=ZD|7l+5v9h2hvW|^k-jyN5RS2(bUiu#vNEUzT2*s2ubwRyPqIQBsg0S zSviF!5z)4^WCce3Q= z7?LNs^`N~?l#>-|E*Zu7JAT=Lsf=%E7E`zzOT0db$p9mo@*{T))gGZ)giJ{5!!W3X1MjB4#GQSB*bVl8eocKtkq7vOmS35U{ z4e7@{#3T~qE}Ta-SH&L)^{2k#L}WOkJisn@F)iBwJBTkgOk@IT8n1uEb+xJ8o{gH4 zU&Cl##L4|8roBk8_|@trcN#S7O9ueoXVnOVS$Aqv|L~b8;TeqCfqPZ7*_qok@B7E* zx;QG^1*M>S7LlBZ8D5so#*-zT$`%Z&qpyNgQ@h547AS*O-lBHopB+UYU22Uy@E zGd=;7t}L0!<2Uo`4sp`Ksyci^oL0~+2+r^p6+5HApCht}~pr#0zIg4LQJnfOyrTw!!#AzK8|G?xZQA5&L=R2UB{#LEgqNSHn6#dg@kFiWm)S^ z)&wzmg&yD9nVE-DDx^Q`Xj9!B=hE$-&@QXDD=am3-d%HcE%~5E+8$HcEd*>n94CFZ zZX|5W(eHd-pGch04XG@dw-u}LfQ|gxAbej|5!aYwm!h@YVoyhX^1iJxf1w?+5Aq}X zjAQhXw~$oa@Y=sxYuIBXOJTE^4MXcx9Vv6(h+;Wr<1Vs`7)EiW|C5 zW*a#QTiIwp?}Be|<|>8gGpmTOo%tn&%djiVHocJ>r%hbtzxz27K+Wlq*t<7mLV z;Jsng(<&kZx=%WUSm3_#)tRaFhIxFC^$7oHpWi>Bb(bIVYPwOiz$k{b!06Ah4k>u0 z@Ivd=$%tF|SQOB0!8sx#+;?I=2p4p#=^ni*vai^N`5yR*MiI`PvlUW>08cbYiFbs7 z*g$-+f@2*wS5G8>tOE??)TiL_y}YR6t`%2C#-9o;AFo?FXxm6K;N zDXCQvM42U0#O1oMICIO}?r-gbGxf&RS3rB#gUvVV@1(q*xV(<;BDR7C3wH}gKbjgj z&eXbd&WlR3U2$I?jOZm-LtAJh&-BlLh{~ORmtkka+y)01@$(TET=`U>ix2hRr63) z;}!!Q&O;}1n9LhUzmQzC^KHg+V$e?W4W*68OK;9jDcO)U6S!G!xV*Nx2w{`A0qf@P) zCKJ@dY*fS=+PjLK#OOHPoXG8F<-2nNSwEr`F6fvRCHwUF8qjSsJu*;lo0%`TZLzLO z2WCg4wH$hr9|mk&>BZXZvw8w?`W+uS)@EqSBaWn%tZE{3^Q`G&07}VUK zHvX-P4?=YKTeV3QE=#mKPppUm(ET%L!rBtiBZLp?9AaW(@6oV(0PJYjiIGGMaGVAf zXk=Xni7(V3m^S8VA`tz#h^0UKc`u0Cpcfd}Cckh4%{O|35RFe@Wif;pOpI@+-p|UJ z#d6~I2s}*}Ju5j&BY1rFWkk6|<#>Wd9Us%1pW``&l=PU+aOz3WhE!;O4tR0!^YT-F zRearl*?5V2V0}~&9reAI$#gZR6N$o?nvV_4y8!Tu?#{3JV1@>JZs-6 zucll{CZkelX_DqhUYrbBCxbu@sR7&;j+cn*h^_$xAdn@B+lEL9G*M(2k|%gUdqofs za(w}N^?HF{|M0~8!sP&ThsW(sxV;8`5I-0II<*hFFN}Ewwx={z@x(E*0eL5s{CH1a z!1lD-e|zDxKr5d!cxgD_?c~l7|89c3_A_$o_TAB-RE`8WbfJXcM3h6o(vZv-c~xQUU3w-#SXF8LnuKS(M_{u7 z#s0722sbR-lmQ~~onU<6*=a%|!kBZg4+s{x%M-4Gj6EE+a$WF8S{NKlnOiC&Bi`3G ztn}dcU{W@SA0G`WK($)^kEfQPS3qU(^IQ#)>#)&)ZGqm_(u|n0YJ!hw9+8X~eohaq z4Mz`3nuJlrV8P@{T0~2>q{AmSN0GKMRPua9E@4R=OJRJ6PVd~z;QF1>;rrL7qf6cN zOPzfuUFG_yohCE2?Xl9K?27NIR?V|tvaqe}EHz`5Q!@|8KAW4FF3+-3ERt(y;3qUB zZPQaJNzqWrq!p#6d}kQve@{z|LYu^p22uFYBx#XNLPuDXSIa(_82mr$RF0C!i1 zbSb3V_?T+IXnviqDB1au+n?u4Se>P39rkKVQOMPOf#YNy68mA4+F`9$m9D!Bl(e=D>#V4 zIo+WQx-s3M7256nlx+_&DnjOzaZff#dHP1BNlo{60#pWe!w6^@xwDLQ*8Fe55U4nE zXPHwbJ?daYcJH+%?2Cq8x-uTjd}eSI*J;{C#kStEVH6muA5!anFyg~*m;PQd;~mc| zvy6hGV3Pb4q4YxYmT)nJ?Ld_D6zW@AbKmDQqUAJZ5@!Vzgt_g_?e^g~%lM^FO#3pv z^!FRVX7(}!d8}6u*#*9ykpxAziULxMGeFO*?QsvZpZLWPr^GG%k~-v9yZj0$_k5S2 zI|RGA?{^y(%ipy*ImgDb64FEJxvwJ9-s%rMMIGyt$xy_%r)9}MS_-W7`^mSSc|u`y z%EiU)H-lgMLQE+4hxl`m%c&czuC2;f6>%_<6b%CE!N)>}dYV>et@e4mt2|N$S%IU@9Fl1=l zaKS5^4x9Il_Bvh2*^enmx$r_sU(6W!PzP?dw#}H($%%WNMDk5-;3e7B7!2|4#ybX1 z&Cs@Hy2Xr@q$ke{aFZa7XUd9lEYU-4XGUHGM2?v+&fu?a2#!RpA(zyr964gR_mmbo z7VwzgYPCwUERziOr(8^$iA^tQpUt!-Myas7!V!+`?!EO)M+p^(n_`Ys!rYvSc+yh< zzUzR}mwEsEhRIRG-M3>B!b<35Ll}Xf+xwf$BF6ih)SBChQ@fWjftp(dX~uLOlE@wQ z#8Ra8&=fW|5yXNT*z<+p6n^s~f*N1S8Y=~&dF5JK@xm%BYuX3K{QI7$?SgIKzL&Lq z>80*Rhh9hCE6)i;cCAN_rV?3(g4=~erQy4gwBg6+1rf^q?spKa`#4tkgUxyG44)W~ z6sBZ(qqGMf0qq*M`M4kF=FI|KJd$nJrKu&wAsFMY^}bm262y{h+B1P4Uh>G-L{vf@ z#z+fybYd6{Ghqc<7%+%&DE>+I-nJ$k^|IRwM zZ&z6EyrkOvvB_e@MBPxxH;0vT&~S2MBr7=~$1KS~Kz}&$svtDauS{$d>lePlA|6%K>zP@$Y?zE|;g!pTMvjlg)|jrEg)-kK)|&I%l+~VED)8)G zJjhBXwNRbQCJ0A9Tp*1dki!c#YJc_M{x(X~N%}j%m=&>|XZ}iOcd!k_)A2By#y%Iq;Mlnwwz-_;@OZ0sD z6cdh12EI$~F8PK>O;nMU7r6zS-7LDKm~#)i2z3Nw6?b|cux$l58gC@;ON`+-!P^fB zWkD@y0V?feDUZuqS0&>C*9P6ticNCJqpKEZM(1dLnE(vR75oQP+I~|%+ zQDH=*oo@+BRYO`apc;pxBGM7LEH5OCyr^)P*ZyYDUgbHwn4Jg&#OHr8_KwlD?AzY% zj6Gx9wr$(CZQD*}>}1BaZQIF=ZQD0%owN4d?LGIt=YAP&q(-HxMpatcuRZD+i&iuL`)K8l6V%JKL0`c3r1O)r5fTDQ`jYs)D~RFqe+Y>k^|>n z!Ad2Fr}ng-Kun-c#ahSOgg+xJmaLEZ5L9HLB1u%Wyj%aFk>G{-n59&DN3B9!Io)7K zjVSK?PYHz#^O9xr@ShL;!Ic;2chlGrkNMeOm(b0%!fHlww$d$uv})c-YS|=()PO%X zC6!$(&!+;->tSVL!Uval1cwN(26-acc?Y5{*y4-|#j$81G9l|OE0EWavMZnraIN~_ zK1<~Fe7EUS#tn+aavi>s8&&*_SVNc)L^3_gscE8GHVj~~#of#aF+7$}f3K>T}rq_X_JqZc&H6(AcqfI5t4o?$n0_9P9CXcr&lk8_hW7DbW)YUHtNOX zES)C6(HpixVFbia2%|2vP=#wGhr|NM@(42`2}{{Y3}u!%Xe?%>XD`8F#dG5iriB8< zN0S5^%NLa1HF!jX3noHlaoinS)vc%m2_owc#tR@P(wi2h7RYGhM zT)`;ssMd}h0vqopD0z`4=#5?iGs|H1Tv^REV&d79Dj|I(i$I4BwMo#Kw9B{WA+(Y! z@Ro!OkdD@wNx_fe4mLms{boif6X~jg+3grmWwzPCj2A7h%$*@vX;oP9hin@bsHs`3 z`@y8P=qpgHXUr&5mgbN5bFE5*$RmHHFZB}6W${;PZK{~rf})$(crwBG(Ze-66XtEX z6y?YwO)B`HLEqt@-w{l)gZpTQ`i(a*)4|ZygJQqrdIvrrmXQ5u_?RzY-^(4jZps0^xVAx5)b9Xc2azb;xcSPFC)3pODH+T69os^8v!HAQnkRyXx@vJe(%eu?0Bentk}htML;S?6cRK%KZe zcSr|04G-Xdbc_9Yw1@Ayxg+dwT(D(Z1+aC+fWLgl6n0xi4ZpsmMcVnV(`D@3NfY#W zetr>jxJm`x^phZN-G?7(-0{Ngf+aY?#XXY(0Yyma?`Dv2;qPDqc3d-xgCg@JOj#3^ zyf(7xGDf&NZn$4|I3Ph!2j{pKR!z6f9mnoBI`bn&z|H}gGn+FfN-K<4+ohhWm$pI`SYH7=EpJ82;GYz+mq!l<3d0c z3cdf^3xd*)l-%SAurk1zpUrmR)m(k*hL+)Jy?KcLvp7^L9w!GqI0fRYh9t(W=u^DhH+_?pmmC&lIpI5(^fVeJq)39V6i-rF-Ett7*(M z>4u`Q%IEzWMfLWAswNq=Nxy0gnSlNZ+06U=>njCWxD863e*Z+K#g(Z}psMnP;8a-f zaJ@ctS4Qe_K&Jih)Y4=nr)IOQ#Rkf%J9iuX`)uDtrxOJpWQCQPdaa0Nt>t{O**+yF z;adLCGEM0@(R)UynZ;b&0A0Rw5!zPP2dy2%#r6LLs(+Khzvv+|9s9qG4*LIDZ~FfM z&NUQ;)kOsoWJX5PST3h|AkBc3t#>RK4<-JMMl`>n`eKQ+y26xh6=XU`ZoXG z@^6q%{})gF7fGk5|K_y+pma#)znA&b;W2%;2l!8<{`VRFfujE#6aK$l_}^rh{V#a> zT>Fu=Koe4(>;>y8S&P zT|Q!lSE!7TNgNk1kyOTF?admF(GL3?-y$BjC|pcJ1B6_;Oi7u)kXOvN!Oin>Vi!ZG zd~>z_DDx`Q^U8fat5Y$w2vH8$e48IPEvmQ&2ZWYDQ8Y%XG-k>hnona`VJ{(uhU`bx z-X0hZuH$_`^NbI5T39Rx$2-(ZG7(^mj7FU8_X>LM!()&wejTUnZLv+AYsrO!(PzV# z4}d42hqz~>z8l(i@K#>u>uV*Q;t@;5Py}~hgWXTr%~I4!0DfAWM}J_@7C#^m3_aQ( zPd{8S>nJ#M~0C&ck*S-T#yaeG_TJsuzN5R zzhCQ$vdbHez+%cl9>0&%D)m+^y+I~7ZdxtdqIQumg3`zzz@_~MW zkze>>)(+mo_Czz568E&ae)-`*D&JDxOey`9(*cg#N5E6Pw-@E=$8EZ8{HC9}2gEXU z&qxObRif=by}kZjwL;zT4oRSJLx|}`YetueBYOw$OBMVv^$Rf7&#`$1}X z7kohV2YndYEU1T6V$M%< z`6Qj@gN-Jtg_SW1B`==LT#TgAUlQU{%x2GGa|{N#9=xL{ZGjqeJ5^7DOWI)u_!12& zlE&f}KMkO?Dr>pN{W~J8C0{HEc_i9Br3m+LUh-H#`8}yAKue9P3uWY@&}GWhzd+6w zIsp4?+w$bCnYy+Lys${sJM`5V8yaG4<^-JP5q--B07*)8q0k#-`g;gE$1&kf%T$;a z+aS<7Ts|;FIswCv;A=HnAtpu?v}h2CZ`K&}tkCcekM3&KWTwvOq9CQ8Qe&$6YL1j= zFi-OmBk9M3*OHk%(}=y->bSUz^o?OA!wz^wmQ5LAs5Y5$#oGial{#L zWYsjl83#1}@ZQ2fdT{AV5w5NHecqsv(D#nb7|wxG3y(MYDOv;D#aVcHr#>QqF`K#n z60joTjbuS%LyZm7uN|uSv8tgF+xmmLRkp6O?5GjNESM}ZPzAV_oH@ieCPvdBBXtvJJo5JqvU$A_h)7lRxv0i7Z2C^<1;S4dnPa)GzIN2kse&t z*TDYyovxr`hdN(|5+S?>{5E!Eo&i{@?!~6PLj}r$_-WsBkfu?aD} zg_XvYKQV?jkm|a+N8OF?pkwl?q{w`$&?Mm>#(P* z3Y}v#G;&sBV0y+Jdwm#jgFCalrDpu9qxn<_Nj>T~*k@VwzW>>thWp$p$Jq6%;bByQ z!Tp#M0Ook<^mE1NFt6c+-b~^PV%P_p?d4~~+ybZVIdWqaPY8W!xTf2*AX2MFZFl1j zU>V>iXffztERQVEH-l4G+&fj!HE*n5NIBe5Ne?9MDYb)=6Z)E3XU^3D-}SM?&&Tzs z3b?Mf1ZUNh8o7fk4X*6jAZ-fQs*8%MCRi_{L8pM1>VxRelLp%PU2z5xcWo(r*d)15 z8WWjERHcNAY&xKR1GbB~yS9sbgKGd0b|&GmanRY({wqcANamo;A&aaV6XX5Q%yuPg z2xus1H7rcs>;fATxRh?CUv;a2G&`vorp2Gc?r$iyGs?Mq)ON_5WNTFKE_;F%!mq-w~3M8jdgOqiWi+Ga)b@3mi1Jr`_ z%bG;3#sysz{o6;;hNS(sp24puttcZ# zDQ51VZ)$F1tEg||NUh5FAFkiuzw=K7`gipE59>ovT+(XX-!1?h9pvAs^54N2?>m_N_4$9~xqoyQO}u|uihu9)KfmCA z9Om!C{+I9Y|8cwi@=5-O^1#T(f=A8B%7{n*-G1!*h2{SYw%>Q_zl{erw(s}EWg{CBSX%WRM`*8e-?Gt+(V^{Ro{yNcL zPwOxHf$=X>;{Uio|44s-J(hn0?|-{9{}L|$^MQYNDEr@*3`}&mIw=j;@kF38)r$vO zqjCLN%a#UB$R7oVLysfEhl3U`RlpZXrS!Y3vszz*gBY8YtW$0Nh-sIh*h z@w|TBe$x3gta)ZmO*AX8p}n}Mtm~~Dz1e_1U;Nz;%G;yoKJnzl&@!g=Wv#Wv{Vny6 zJ6J_~jq#}{X+zK>;{lfcqa$~v-s>lO#b|H>BX0)%=W3MhgMZfRow;bWu;Zemn8(!I z^r1Ja>FCjX(Z?*SDf%nJ-pcSXluJ3u4gf#OP9-L zMwFLV|NDlP77%Ks!?VF`Q5I)&#af5`e6n!p`Fh8Om$!EZY8uBgC%Zc>$H94>Ph8ye zYhz>-5cV=H$M#9|XISl>?NTQ8U@h*3|6i_|%qR zT=E7)azTO2mx;DE89Ci{FnQ}aW^*i%T^4=*U_x`e33M44i3FGyLLav zD90d2KgTeGaa!$E{e(KIdWu>y6=gE>c!=?Mohf?_miiQxYBK3~)~G7m8rccbw;D*QmZ;i z{e+471Y~*=og`Peg-U-#k-DlW%tRaa+2T2S{YyPa{pc#DDQ=cELq>LGnl{th=J{~F z?zH}N8msAQCbA>oA<-24blik^#@*Vwq2tDp$As~r%0y+Br?Jh}dR%MwWY)gHy8HTc zOJqxAt7Hr1_Y{t^7R}b;7Sk4b|zP=I`dxjMR&F|YC01g4J!`G#@)=rhZ6@fYo0EwSW(!f9F@HtBw2Q(t}*ldjq1x6B3yTL^2-K zkVq+Vq9-u-TZMad#St$Q-bn(6Np&m_)**OzS}L9_S$9wCtS`POm*j0q6*S=#FytBX zpXP(DW4C40i$aqfe}J5a&xSFFFrcdIst;H-*H@MZx>{Q%J3nlW+Gy6YKRQXg^FPx4 zf8+P>wDw067K;Q9e`d0=-Rw?fwog4V;9z;P?ZIEoCXk-xbogp~o!=ep(Ejuq{5YLj zuJ6LvnRe5DKfG8&-MaHleIxvkd8qd=ZoeFM>C9Pk9=fQ=aqhnU9F$sqc4eGOL3Oip zx>aN=`cTqse)VR3&3IJw`#kRc+Vv)d^=^iXlA>Ag?QZig9zXP}6$kRRVN@G$_bnTC zGxJsj`#Nq$wY|XVZ6R&zJ+bMWDxId%12b^rjKN;3HN0QBJIc?W&cVgp;7^}0v#e`( zo3%NWRn*i}RKy%zba^^E;cDFI*t1I-oSOWeZ~wZVUEXcf@2jtH#cjr+r*gv6(Y*)y z<<;%Fdh>ac;@sTkJ6>9bAx&(SiGfw!qsjZVkS0%F$x1 zI8$I}zf`XQp3`x%4)H1nwehY(u=@~aDBF5JzVZWl3J_}20*dsxU%RBs#G?bqP1ktT z1wRe!T5nX?fTsFcw-~y3Z+_p53ijp?H+P)^0D~62IOVK{*s}0|KV$+Y?=(S}60dee zs6{`WC0HDygpAw8V z%r)E|XrilSfZRJc8ZmH4Q>1TWzV;FK+Ia??#^q@!wyE}-BAgIGWRuw!7=YwOGD(Bz zDW!&?AYx7&l)4br_(yZGsk+m7=kyE?anGgIJXBqDq;1X4NF}QzBR3@LJ?Wvvs?0!w zs?I4$kcauo7|Z*G8Z&Llu_BT~YAA?n(AIK#8TbkS5{F0jv>3uR z750NLTh=l!UKD8$TpY^lEl9lX4_gLAh>r^dKS zlp`SnUQ}Ih(INV{pkkQx&o_@h>+6XeTNk5F(O!tka1FF*yvcKGuK=NtEwkVLxCA~7 z4!F1>cYU@CMWUg7&ife%MZ$Ze%mb*x+>LNC2_Y|RWGLq!2x5b6vu8pGehm%j2>!>W zb=KD2K*W!WXA;0mo+I<=H#$&7?dR3kz;Gw0v?cK1V^Qf_IQN<1M=n4sYkUz9F&e8E znwHrz(6VzE7v1oU4HFwm9M4#&7yCPzExkr6$kla(vHKhf`br8z9)=hzhr?{&n&_BZHO=n6JidAgilmaBk za~Z?xsM1GCC<~Y>_37MV49_ibx6i|~njf1xquPz!RGuNRs$t$-;VXj&o5k?<5)#crVa~S4LJ-ie$`)PdYBb`i_sq!)@ABKQ-UG6X@n~pdf zn|#R{IpVxRSe)2L%}KyuY^+Gsr>5#B4D1OMgNNgKJ4R^JDw#;TNAc+gl-E+#Gc0!;|+;*9@?1Ke&5vGl{&!xL~^6tP@jTW@wMuJ|vsqEQh z?4|*mr+3Q3HEN-snsYyma6iDI28)=mO}^{&@J_d~(Z7=IQSCT0FNCbYTLaBr1Fdx7 z<0hK1-QEXgTLGSW--?Jw3K#w8h7;s^MeoiEicdYa~ zd|7YaiD3HF1bxeUrhBRgF;GWt(zEl0TuTGw2zK%I;>QF_<3ogPL7!m&ubM zM&wJiO?$_OpMdFnQxI$y0^!n;v?m)ZXdl!!lj93{rbdn&(#7L>I5%0-d|k^5fIc>A zoD83j9BM9|(nJ;bnuw4Hr-2?B&u==zML8c`GR};~1nm-p#r69T`Ce~u^I{`U(Ns4V zVHKxy@9Njm`g3$!OnTaQndqlm@PBZ0+j-Ng!^aw{Rze4@*{J`>v;;HG{fLLs%BgAi zn%UTHP4A7}kus}!|4lAEJfN40Z|@J#qG=+xYz&|Vu&TN)DjS_?Q$y*5iM2g!K_q1a zss$mI1K5I9Q==USQD()ZU06kl>?iBf;rwwJz2&1)uD~>6Omctbe~_;+OrPwMta6W^ zI11isK#?eJ(N0`A(1=s|jzRuV6zFBw!o0gZhzN*kNx9r_Gl+j0q}3g|T)ni=XwWHJ zmji&<5j9mH0j!FIJmw+Ysv&VS8tnQ!rN1;AQY*lh=cni=zuF)WT+AS0UZjUxcpdLS zxy=t07hFH!&t@l6WlykUzde=TSJEQnIf0lRu?1neBdMT@T8T&|fL_bwp*8y5JpJeq z(!mxPCh|H01J45yLAnTEh&4LX)}E&x_*20 zXfaIEPI~k){rZp#6z9|YUZ_h++1zgvoLgGE3IJREQSOsN_>t^0W1E#HvHDA^D4yP( zp+I+Qgb`k(Hr}VJAe5r0j*cQ!g)A;aSrH?7M9}CFvgK=wse%vV&}3s)2Ymr(7|t(z z)_X`YK_T9XLI1hXF5tK-nfNKrUL1i#9w~t+*C4^_9f&~oPt)q1Z`+6U5nruc5Uz0( zCHxc-o@Xq@8J;^7&&~zsj*%B2@0{IDS2140gYg9H!17i(qfVUq$VUk(~?C zd+Lg#R??wR`k|XD^B(J1MyM^sr!ynxtVvlz&PS}`!3Y}3hi?vKT@Q>7hwwZ*!a1*m z;XV>v;~7`{c6smC_oT3L;6HaW@v4WbbzfVVdk30ozQom=Z6`)ui<5KYyh^qk;r424+ zH}>oJ^R*L-daLi1wA%<*UDIW6X;I!STktYZy26-NjAImzBtQ=N5){{eI{uV#@t-_= z-lGnvsd~9#s$-OwR zAyff|eu15BC~=de(nTzalDHXf8g&}6>(5)}{|Jg!oD#kibr2kS;JemF30pY0F~XOU zqQl`2RnQ3lW)peA?N2GlVAX)AsD=jZAnKX8!q8nOscuYqSPD$TNu#&O=_~1{kMU1N zGs>`H12RQKwVI3c6043srjW-KL6#FPeRf8Fmil=JZnTZ)o>7OvZ*>Qtw^j+pAnJeB zOm!i>;jt+{sxyiKpA6hQ+rSE+CdQ~=aK3E@StAa*)3Uz;;baxQ z^5Oh<(Z&)2Ldkquf{HRnzVM{L5sv~TVbAiZ1_D*pGLdqGlY?a;Zur^OVEbSo_>x^v zs{8W)lpRW0g76khH4YZRywUip`nIk&9U?^x@Jl~#xKyI0-~ecX-8)H&s`KS1-0pWp zJ$Yn47VmN2JQEiS=?9Vdo9v1m?3g&G+-8mWxfC{&?%0){Yt=>SDc%6F+6B2iYbAXq zQzeSeddu)VKkUbom563?RZ86uVJiwc?v~dMFx;^5Ar{ZBs{~SSKl3iZF;RsBy(0D8vCiby3d32ze^2+mfVt2{ZS03>=Sn zktFDqMVQ0P78DQ@7wk8+n^iwUh4Vv%qA%I*DlrJxY4!3n^(EZ=2^*fNC6mrWG|xpV z9&7*?zp0(!2keeY@$V_!GZXBaJ(90s6khFte>rg14xARYf!W70P5!zt96y!okLGOF`EmY=+`nW?Q zT;ep$6(4T*uYTE&$7oPB4Ov_wfe2gyBK)#=<})+QLfRCQ`|(rPyF6$fKW;BQZaIH6 zpYt!~5Zg-l`wZ==ytEWi6RXP1TZphtHAk`GHnZ}}(@3f+6>W-~Qmp46>Lv$;_P7Iv zyHa5bq;ifV;uOdLo8B3{5uV5yyoCe%hQ*cT+eVNuZOwp67b56Gv6q6S+)c!axc}KM z*Up4cagWyL)+&YqVh7~JDk;6 zE!O16F6U4_sM+MnisbPP#ag@^<`evlU$4q=grB;r zyU)lnW0tn;54O82bEuO)8$Z9Ayik(gmv5i)gmUAp1%MF4IgzFDflYs~4(8H|Czg(E zS7-f>rpqmO#GMIKjmdM&bzkc)q!BFa06iP>jh!Ns+@3Q8pZhJP2Uo~323{&>s0xj` zk43ZLJ~q#|Fp9`&dE$IY)Kxrso|b}Fap3dLX^&tl7!kr<1x?m&@Q`W5t*Xyx=4p^i0@gX#6c(mHGNM%ZhGIVG# zd}i!yawC5dkba9Zxu*~WEV}kSh+230iKw^wV(lDY`YXGeyXIXE-cC-gq)yr z*#lGE)`{h|^L)pTK`2-wtxUWcch(5GN$aiY(RwiF;}*aGOof%fLHTEqaY1Gol?>^6~u3C==VaE2*dy+QQuZZ{k=rA6Nz+C4$aJA)-6_HaKq0lKhC8O zONPosqj=XWP-+Y~I&nZvw}u5qlkWVrChlZp>#SxA9UXE+5tg|1BKgXrFdRIQ9`b#> z3HTrJz!JN=6?CpG6ejdq0krby2UI8{0_eF+yD{;y zc&ESadHQ;>CYRm2p%{h~G#RT5^mw=?B(ZGZT;XAJ$QHN@ZZ-U+B*1NPq1KRK)f;*@ zC2x^5Z+}kgWO3~KellHVo+q8rw%9$${Ln)*%pbdlU_b>KVj{W3=RJN;nZ<+{VXW$O ztt>e(Asy{>RlQ2F-x%Yhvc?R+VkI^XcZOe~W#Pue!wkKPp$?d*23Fx$nFTX+08F$W zz1%~8C|!=D)a1Dar22t*jk~Uc{sa{w<0TJd3I`YJ!J_GsF#O0_sS(pJ+??X$TigtL z?Fqc>TMc%`07q#dbd$FOXxcL7gZ&s>8JL7^zj?U{yPXg4`%98{Rm+39Q`X-;|E(v6 z2QGH5;zh8>40r&hzJbabV~$6VF-S>75mKQnQ-b_}yn!e(abZC|TF4uOuv2Di8}fxi zoh})}tgwj`&vJSdTk|R%;9T8TSQNi>Dj5}J4?}}u6pjq8mJ*dx__%(*=>s_jaybIR zr$T}tS{~Q-VU$ZP=?Apjg4c2tRT3&??L9pP2;EJp;y`Pv_YJ5LPgYt0Z_1`zB3>A2 zv|(PKyS0Z8Uph~t!IPgV6q#F}r>TxO(9JV-UFo%dP$xltyrcJAI52r#k zv{^BkSIchU7lI@tUv$SHu)GvLATQzDy7LD>cRWI)UEA{Kc&Bvv?M@iI&*&WB6iA|Q zggCOz9&1wrvlSos{^A69%#eI8wXz}N$@onStn(637gb-8*3%h5ZaQo+_jIIECQjF5 zmp8pDL#qdy$=0tj(OiY*3EUa;p7 z-U`k>$6Y41;9}c0*qtNPiz`8qVWI(dPHR!f&C~r@`;R!qV*n`6o})1CRi2@_k39%l z4eIaQMdM!6=x&*NESjF|w4unQ5?j1L79YnQG4x`RyJoO$YOe$)v7eLcrbO^Y(}|6r zN+rTJ#0sT}0s?X~fo&ei1-mw!42FLpKqJ!Lj{gPtkX)u{Eq+=OFF(WZhtGe4UznuJ zhoVao$!Jarojf?_O|Gf|*Sf|alwmcbAZ8qH*U${;08Ez%s|EjrkiwQhihA^UHdl&i zPa2Hz1}$qKjEoD1ngpf9Dj&^tw=RWkHgX-oVW-c2ooBiBkM6eW20F6<*X6V=6=6M; zauT*120}HT9D*iX3i8+O?MCF}h<*pCrCYK%kGy=qZ@FSMM}u-I0T2wB2LCv*5&YOpg(97n9q}XN1 z2#r&plJ^_iTH1GAP2sEuHdF7}p}x)WnMORVB)RQ1-C3W)*2>2EU6o$!^K9=7&tTtC zL7zkh`Rv~S&0#LA1zgLm80CjaVa^37H$YW23n3 zfQJ%E=BtO&?<)hq_Nl;#tyd@+@F@Xk!MG4EGN*UH*Z7+iAA=%Y5y#Rn#wkZARc#K} zRS7_?fvqlqNo_wOp!HvSX4D3MyKwaR%{OdaqZMW*!!tN3Stux^OD4go6GJPC5*RC9 z0*+<<%-f|i*Fwk=vf}b^3e=B0vEwh}qRucXIfhI0BS9B>9c+Kw&cJ2&H`Lp4RbR{s z2+3Oyap|D8nsAibo*EF_U{-rr&D%h=554};ZpYuUkwVWI){io#Bs)h+Oi~VBEmlco z4bQ|YchtN+IcvNL0$2cjLdjoVA-WNlNuDxW8fBMwg<_1<$qFwiSQw7PPhS_J!3TG#Qe)R!&8+mM@)b4Jy^LlywR*_oUTtJR;TR_2xTGhTfVs3c#utRE z)V-ZydJ>8d8KOtra63*Zktd<<-xRgrYGdPDa;=|ZE)x?&OXEo``?H_V7DJVJX zGCs=a0!=enr1FXi`srum+i^3+YR=PgOV$A@W+g~{j8Hlg(CFGW20|TE*Vstj~25DzBY^&D;2d;o<{_0zS zgRVfDu-rtQ`G$j+1Xd#zeG6#19S`6GvYIA|9^hP(1ZSK#ROU$yay`E#OZ$kI8BYjy zhm=d}qNPlYN(S(;4~E%sq!2xC0S6VDzut z`Ghh;PVNkD+t$RFqnC};?KjgIDZ8P$H_qw{3b>yTzDQrsS{y}QSw70_2c~;hH-Qt| z0pM&|zt)QsZ*N4U((=Ug7$DrD7VziD>Djmfpnuv$)e=oqGDZZ9KJOP)NOhS0ezb)F z*vs&No=Q^iw9|^d3 zep4tv)s);m|6pW4j`*|s3x%=?#ghBi@>n;#pXac{(Jk@t&eVmaBL%d0Aub|zSfrT| zYJiUt<~Y0oeU`Vl_W*9}T@`GcwrW|ZsJyh}C{PRb0qp$7WGuMB9wK6XJ`uETnSOy( zSp{Di1@Jg?R)l~w$k5`2kTAC+aE+sr^*03%8zcF=wtH`*wbKwA9^U(&u7O6)KTN^Y zLYU70=aYoFKIyLJsQSai1=g7t&*xYJwz;>`L~?t=aG(7N3jQqr);p=TR0k2=yOlZ^ zP0jm@oPq&o_zSw-lojvTo~e?J|D4;VeKPUB38EFrkOW$!INcAhxq=$|*!VNAwUA2r zNM9=lrzQ`xLd2)uLCq6@-dwzGvcn4r;}*R}1>mrRA_*-pMp{G+{>QK1u<0yfV!uxc z3J67aOvGqjZ%fi5b0n`H65e?MKv`h@lYsHEQ!8HP&hHUy8BR5<^zF!(cI>Lo(Q5Y+ z2Ww$uY5fL!aH+nKt$?9WxC7gp&mo&wiHPgM&-5;B`F?;0jzI=J?TnBzE&ufoB6YN|FcVS_RtMzZp(x%YV%ZrdMf7ox z{1Hff*5gXAgU5snTBD5|6!Jo#j>bC>g_Pp}w2St4iyTOGpj$U~S|Nqr6N&bl)#27g zVHiZ)(8#9B7t;{;LuaUw^HvU~cVN%QD@c?Rn>VQS7ZZ{v*a>02W*pIMyO|Jn>_6zTO1wHF2&&|EP zi9Og$L+!(+BbJ>iSCA$PMrfusIox=*3oB~?vR+~DTJy0~{DsFeeIKwm9mh4HozI-d zt`k{4Ux!T3Zx`znoE8RvDVUm`={D<*gAj)#sIhwmxzza?P{lzF5dKAP(orI~DZ_mu z1*~{JGhoznHHU$Y@x&bUc-sFl>ITecY|H{LQ_C>_;T=;XQfp!-8Y!)Rk+cCnRxK`hUW%0F}fkVu!^F27$zr< z1X?|ZlXs?$Fqvc|sGi}<>hU#Z9K`o9HF{_AcZVSHYku2vod)R*fZYK1mQZH&6tQ_4 z9;CqC7^M7J`2=th2BLnleTrbMWX)fh8jg*}67IBl5Z}!PlC!B58PcTYkc70+ftpG5 z6yYgDl+5NN<|yhh;L&8)vZhcy2U^;2MPA<7%y1XyA&1rfxVMq$#H;?IUkH@8TP3Qy zHOmPFcyH)3>8U#H>$<|YJjNfZH+*L%%EJ`6ut$t3oe)GPpc_50>lWo|-=Le7QduAA zOnY15q*U2cO?2W}qFKRMyY%Y)gxk4HN=K9)GK!4&b3-rFT*FW#Cx>~crYDKjc3k(! zMxHnp$D3FVjWp`esI>WvEUn7*3GrJjCFGcT<5iz~qktwg1x;jrIBe9S34)@pJ?@R| zw#}d@VdiIjr*tN*`kIDz6D-@gCRRzz^fegN+h!L!n43keQgUX9^lP828#oqm-q<|I z@IhoZl2%RQ^%k<+adpJ_^$*94tBrGCwnya?PV|+@213ygz&sX!#dz+9gpQG(6*Jl^ z+U|8XXc<=SV>cGF!mH5C`R_R?Uv;0w0MMKJYI3u?KY9t9O2R0tdU`RZVxMevx)+n; zDUu3HX0`gjsI+sdLXHfbpRYX#_Yrslz?ypNQye~!?N4yF_j@)dYOWzzUf_qLAdy}n zbZUi(m3&yMHilm;whuOXPaU?a1-h!T3WpbHRlL=R*lELU| zH=iMo$Lt|94LFPeqH;)oLd4_@2qMt94JGf(fh4i1Rd$X+1gF6EP4DC_4qHvVRz1)@ z$`2KK`m?M0oO{{5g|dF=>ENp+e9w51Eb<28gDE?f45mZ2oup5@$_{A{uO$r*=_9`~D z2r0ND%Pxo<17${2#FX6S)5pf|$GNf##G;KQ`gt|&n|=t$pd^rwqn?|Lcg~8?(&=84 z7K=~K=|@G*>G6u{WXZrHY?m%<44j9RmiERI%lWcqdUmm?VW*sYG_>Ykdo0Ex>hfCR zcGLeBA201EYEm*Hm_jD}6e;;gGllQL+l`>FP%fD1Yye>E70NGyQf%%AHL)9NqPA@!!=L*AyF z88qy~1Wp;ge9*XkN5Ejsg{GVa#uuVn<$il# z>~v4s+fucE=3t23B>4i4-%q#-G=O*gQtb+IWV@k5it)eGCQ#0WkNwJ%T7}pZ0=Ex=SZFngpYi{5fF2(y^M2wZDoo7aY2GvVWSr zSQ^~e$#U<5HeFx@AC8;Lrsi8TR8|0p@7q+D<(Yl%WJE3R1`wgC)IjcYH}{aySdo*m-{fdki>Y^dUv_K=RPEUU z2uiGeiJ3jB5`?k@*z~Sbd8ZDjSKU%Q!L_J93Q5Bk7)CVciYOKfUv+32=-a6^^!PUvY5dxB;YiuA0l*ced_oY5<(- zQ1!?7#dCMT>91kg&gjTbX_7-<-z0>Y!fY{LTt@9{WBkSpNu9HqJFcBL1bql83?RITiH_&)*+i3|# zBst#PF$*^hDJ0%uCBR{hOx!}oA(?~}7htaetKk)J16=kP+v>z;3GLKYt?AZIUP(fr zYwIk$+6V0!(is%1^X(0q*vAJZY^a)a}Pj zTq|E?-c-hd&!0dDE}AUFWyH94@#}=-h=3$@X`m*lFFW>!YL^?kSOZQ)V>8%1`P942 z*yP+QEyfjoSMQ=TFroI)RFiw!8@l#%Z!=;5TUX*M$ah!tGkzY%agT#7VTgo~QckEr z7k8UiMqa=UOBaD>u}Ht+LIGbw-nxA~G3_kDEV?H%N3gPYU1uP$F@Z@#?|&_q&fx69 z9LncG-r|TJ<49x?k>u1y3!(*cy897Uj~L3`QyG!=%ydTn?vLy1**|8og?oR1>X8O1 zMe~AzwVEb&iN1H5a*{@^B-;oel#v)tq$e?crU$ z>SEwJ@8+zc8grzPhd0WDV#!HBpyJ` z>nHe6FW2`|ZXTvr5Q8i`Z&J`HyS6kg_gXzn%}ZJsy5X%e`zc94p{@Ymoa$d=S8tYG zQudFbFAI71r7u$f(wvtWGLy}Fo@8htl50hWn0(qLAc}V&^gB`XAtVJdioJP!&>eH5 zJc*EBm2pSad1^ow`=j;m#ZV3W@$c@c?WZgXv^7gSLeyKtQk1#cJvP+20|zAsn#VXo z+IS?q`nr6;Wt^=Bb&;giG{zj@7vSyrQ3%R$v}^(?dZ$$M>;pWQ6J%OqHn$}>c7jBF zftNq7C=6~QKsZ+!d`Z@EavJtl@;8I*{QD>t+v$Mss?fs$>ci}NR7*of-bU6A#1e4s zvi1aoo5a=DHz&o5!NDj?*Ap64flKq}Yp$u}dj~>4!PsNa=*a^K!9b$Qdygvh*2?po z$P-H2*v447p!=75xi`^N>C3Fm0VUJ#{XVp<`4H9q3;-szad%is6?NEI==w_y9F%&m z=6VaiKfPJV7to`ZxFqzV5pmnc2zw%8ycc(t=FNZo!91P#wG*Y_YoYl@(etXg9cdbD z?m+%$3GM5e9<3Y0=5es0y0P4S2$eRQW6{Ol?suiN-pp4ps=I5_O^`q5c_vUn6t`CU6nhh&?e1Pjw({{6Y;)NyvsP zP8{@)#k8-qdI);79{5i&wT)lq)IVnbG$pgDWfU_m|)Ux<4AD-%j>#E?ce9Q{I<$l1je87dpmN$$|q@)Asfc3*;P zF2U6fi%-Etm?Y3loJb%D%LJ0iJ5Xo5yk2Gim;zu~0`^oDSf_jBn@rU6kAQ6}!SUT- z`YAVl>(TaY9ZTS>Kp5i*4dO?^x-npO7bGTEsdAH1Xx1ImWuwq6jPO70y4A(V>HwVa z%%bXxUJG;n3+wMry*Y?j(^V(}PJ>(bq8uq6MKPmyfcyLJVA(HU4xQ{c+BLx6{rqJA zw%cuwD*A`Py7VeJ%A1f{-fFFRYa;9tLnR)xLHeO$O^%{TY49#mVhsLB?y#(8F`2uy(-VtWI5LG z6GLMpE(CnQHVvAJ*Lyi~birE*@GUzMF7txN{Sd=GIcLfQ{t2!>n7~i$Tcwn!ntZ}Z zat)x)p(NFE2<#f3^5=ob&B;p&k*@~dusaGVl!!m%BzcES<)g3IrpS$WliOe)^5Y$ICNtFku5e5cPjK&#in|L)K`d1UGNe_WnGghH0jP)`AOlilru4z%s%s% zy9WvPwXCVM0Fi-dJj5!5+u|^n<3I(A0|n37;zB9~wP@s{2NP+sLIHpaWkF2T1sv*g z;99YDtcf6Wz2(XE=P!6#hHnZf@>0g7c)u)a$!RV9sdMmd*>_lWkLqi%1qZV@Ey!sX zpjerYLK|}2F)lcCiGrVcZWXw%KX0`2M0sd(e6HV{S1qH}Z|i?i-&2D^X+O$Ei!iV& zb*fxJpTLtQpV78dLQ=TF6>E(Gk?abCbVPDyt9lEr80GhPvDWGzvG-SxjV{yG8pZ{C8LPryMRRLBmj*o6`v9Ik}tx2?B@b3FHoG4Amm zty@=D*E`ftdS-z2_xajvo;W<1vL|-3GkbC#s;g6b5nj87i30;sc^{^-?cAkuAMz+i zUR=mD6B@Q;4P_z1g{xzhh41%*0?Req))hpcJTv&->-yVOU|QMtsS~MSbtzolyyV%k zUAy*M!H+Gm+=%qZv|uo=>vr>#C*ucioBm*`$xq47E?C`itmWvP!TT*QZvw75Po`F9 zV$cQ%dH#?ab>4FW#^mSGxdd3iwufw|BWDhnkJ#VyeD3yLEp zm&zf&ElBSq!A;K1ye!p?0#Og&CMYt|A`}Cz*Er>Al(>i-=7@r$C{!l@ncXBUk(Z-( zh^a1oJDjV_LWHL@lu7W%H^goZ-Y&3(2R0*Lvv}=U2Q%Mo9+{Du!6^Y?*C?3r>}xQq z%p%99)~?m0-gVR}y(A?wHxGk)!@HlE{!!ak+>k!C?ct(>3Asg+j9OAu|Obdmbl zS{U^i7t5UNAWmyegknt-PPi(MtnQ{)TwvK0uBBJ%U-dvl(J*OYk}aTJwR((8ow z61%BJBCH@0R`&6fTq58`cGK@oU|t!RUW96}$rXkL{20hO4kOM&5}S0_@XRD$LRY~< zwy;i8-HdC?IJI}PuNB`(DVHi#*@p`FLf6RiVOBP9=JBPwwq=HA1*pQWcNA1r_hDjd z%Iz|)oeycAE7;@z#sc8RLHwKWg(;(oF&UVK0U;Ez zxjGBfV|og4>!;9@!1v2J<`Q?I2lAJ~N=fA{T&)61jQu&8GsHR%lLU0{Xnc8(2*pILl*lL@>-+6$z z|91G*;~Q5IWJo>N!FRKEa6EF})b^s=Q6~)4a;{11kd5s{Kwai|8GpZqv$r?~>;|h_ zHC07|yt4G}R4_eQ8LSD&On}qk%p-!Zut=vQ$R;2bsi-00>u8>}*h%Ct#c?-E8XItJ z9;fbk^%G4&Rx@(J$p`ZILdOK3VDpQRcX-K?W$BUG0V$!qgY`GdJ28nl9c_wpz56vi z+R#3EkDm7VAaFnjYy}#1pYr5jJ9^SuJavBFOvFjtlMa^n2+pxBT%?Ksl=~@4s-@S6 zK6V^D<_qUvgnFLG{as8?=7*(4Wd^hj6|PNM z5(+%<<<&yGxTw8+zRI$85Ao7Tq^0Rx`PQeVaDY+-(V~tOiaO@>EA|Jsx zTX%2mFAau4nCs&^nXaW== z(f~|+>$;?x_^}{knABP3avG2sluwzymz%_nc)yu#`>$qPQ7yefXjedAUL%t&FA|7- z11MVp+Ocf|7?~d__h$kfaX8NLmH46j^Y$AM$1ba(B)OzGkIJpW;RHzI0R=393N{y{ zcE7m=rgwm8o#@5K00{2~GITiqqP0UT|3=u&vGJ3lnkpj7%S)w=1W03BpR;i|WMyoB z!1+*MR#-|znm^by4CcK02`QiQqI^Seh;c4R{E@}qXD3xqh3LW1Oim7!EV(J@JyPw_ zXFFl>%>GB$ zG8|0ffNKkZ41F(SpEl+&dZDJd?d(#vt6<&-hm2tA4Yf+++Nto{=8~Y5}@nJM}_@85k(HMB3-NcjoNd4{bNvHSbT1U=xX)!W}{pi)I)DsPwb5PGQ zQpH5K&dg+jEvl51_u$$KoGa2Q6b`xJJ>bhupy~u0d-T2=25Sa@VgPI$M#5^>YlnCi zJ|6_|XF}a^=*{t zg^WvEj~-r%iX=5XxK5ALj@GRgh@8DCb7Jx<_62W7?n26)q^=hD3L){gg&hY)d_lC%qV%>VWO@k~DqBZ0VT(0qGK@8-_+crRALtc1#WO^W&|J~Q$ z4}%$9KQ(~qgvde_~P?xik%v1bsM`jqLP#MTQ#vj|MW0CeJ9H zFLIBzh_thWB&rN<-jp9YRjzvZ-N276U^WKMvJrI`a?*Lb!*$n+)6Ez+vI|&uY6@{4FtL`d7kL;PqKwN98b>$iFc6K^$ zZrtk7cwrnByU<${?C2!Hi$+hDB1kZ+qowq#fqUln902;^<(7Lk2u zD3o#{R^7hOZpfuX#bp175qn}kyBal}XA6$8 zEzm9XB~fiS)El?wcvIoxV8I}mtwnrG-&DyIb&b?jKet`2jq5r1Y@nB%4KA#I9bq&|H-+E4z=vV9Y^(` zOmiNnLKviqt##E!^N_<1GR7xA=NMC==n~vvi;AhdVO%TVbV|$6m7X6r`A0hs^-Xx5 z!QXG;_$MT}xH-sF)(8{JqZIy_xTaE4MEsbWagWq(<#bDH6{P~MYd^5-1*W~gq)!ic zk7$pg*fXgsF6QYBIBn=EUJ-~KebH5f-+#D@&ZOK?>THR>H#p6)_L3q#a6d;;Q-Sb? zZWsGTl?1*2y^k2fDQr2}yAarEk3u`n767b0TM`n&^tCZ2IJmsY)i5%*>W$qNV1x{WLwRa+!<9A28i_s`(_W>$Z*i03c1 zT*TQ6Yu1aMfe>22+EI{Lt&JCe2My-OgD^d&!t8PhKfQdCeflDTWy;jdy+k#uYrv_b zh)X0Cj`7m2;;(7^3_FUBH)MgH#J8#vzoq9(inH|zM%qUyDNN>{&6DP__w%01 z&@~8vX$z(L8*S&y8yS6?h$u1PUl=CFT9ei0gNtUwEWP6U_DP1|<(HNGRukXs|a{Aj<%*s7Zxo3Q->QIlv%h7)tQtM!vWt zN0(?+1r`W%vgp)|@O*X^k9{mdTPuKkm#3J;-F4B!Z%npu0zQrJJ${vDkL+PQMRG5xBMOQ=~J|u#|+*zh3QeeVIp<<(}{KX-9^ls#mWi%;OzRxCfpyJSl;FQk+HYb zwbk{MS8MepWzn4+Wr`eN6m}gdH|xqusvuASq+|yS9QO+oNw2^E!t=Z6c}kI-3Yik} zzo22YUEfIN6R}3<&wbUON`$eK2<lO!x+uM5U#uuLa=ll4@@G_ghS_x>tOHI!%o&=&P4E5Ei3LJt< zlp;m06ahO0Fp*7L&v#FrP)KAjZ?B+7i?S6(>Ads(b&ZV!G`y3@PZA|MOS3*JG=pzo ziYjCuPCUKsv^BH*TxIH)m+e!f_(}R>n7jRJay~P(s*4^W}v>yy$a!lK{)C)JU~T;UEQ4-5?kL6TW!8wuN6POoHBm^+6$tuI53kPW0ol z{{q>M90Wufn@QcsGoZrvb0Wn4vT(65B%Yq6+~b23si~?nyx{M;QBR#pVG>MH1Op1< zg@4h5I_xTi#dSmmGf&v9S=0^!8>?U%&U%U}fai@&4do`I{bUU%Gdf$E5>G#cnujaQ zrY;)4g_`YT-hy>BSw@ytKB{aftuqYJAE6#tbeqU}Dos!Z@mNn}v=EOA;xEm^Q0c5E{YiRuzCxa3$t=mUBrJ5bw>3}D?*A$1oHve@ zeowo{jjzZTnOm6NMzOR%gXt|Sjr`YQ`gM48zOa0h!BtS$SD{mDaTRi3X8{toiXuP2 z2&qDDM`v)*$T!278TxqegQ3+wL$fpUmszY{T(drFVtFS%!}QgXvSJR@L&>C}f|9Ti zW+?D5K8re_;LyJCyjvovhF(Ix3FO_hv1;KN z?%k(>@r;9x^FxrrDVt`v%Ykcg>MoeS;7_LRfJZm+41Rwa3#LAwSd9-}a2hUG zWbiY1U1>>iu~zfA9@LF$bqN%oT5WhY2PO}wZ&$XU$OK~OKSXKkn>8Ig=XL~oXTkk! zKZ;LMYm=bYa)DfXq3abD3i;QDoCbgBdK#1t=&|-tO)hd)ktA2e@m4Cl7^i`Kf5m`s z{BkGbz2orF%^tf??+kl4wpOOks}_IJbEW6{_(zlDqu1}?HSA;f$!GDAEtU3)DwCPs zS?}4fYv`#9mYuA!N7EVI#pp zMQ1^y$KHoO{2p~RDJwZi~%t)fYqgfMPC`&%*9rfGzZ6c*MbjU<%2 zu<{7=A^z=siv10*W3J=h_K;@}GdJ*WyD2t$Wjn(@jKV;8m?$_3mxmPC_+jwvs*q2d z1}Xzs$^f!B&vV{D{jCJgy9948Cd0KJufb1~>Io3rMM{xCswej8GSuawzV`uzt4WUJ zx*eavDdV5c%0UM>89DRq^g3tQJbr&}etx~*&h^i%a{qn;{~bG-2Dp9e1XtkpmBCfs zkMvhG?dnB(2{pPd%VkQDn4lE$JdwH`ASdoTyh#(OpkU;La)YR%+@cIQcQ1;S5O^Bu zyn4H)iFXA-c!RexmVWQ|UjP(R?*LKsHI%nM>r9eQ?Ax({`#c?KrFqq|@4uS(#ex6W zzN^je%3fOsm>JAFuSAUc!bNIjA7u(Wv%_T3(cXu{UP>^hS7ugAyQ`{8E#+hN-OY}U z*1@WwvODxGR)kJkGC#+>igLWc%$JsAmLyjd9L|r)Nu|@zr55<}eDbugjD@yQT`gqx zSJ+jx{ahzarUGpfzYu@~xH;u0SWE^VbQi)v?#lh3Qxz-ae2vu(%S@J2!QcVp?l%E5 z<^XkaEgk?obO5=YJOEGJf)`@O$q1Y5p2?j2a{9wLKPX`Do5crbzPN;&mewczvi^Zp z?n%xcmJ=9loka~Q@r|ejuB6S?s-+VwVNWi_qWYb*ZD5Y$&wh zLjDDUiXBAmF>>}<_8{ftIRC(473zZp7NCxLm7|A?Pf%0p8jXXm5GIn5T7cYFIimrI zCjgndQcfs2xKHcYa#n{zp~V4&P^3=s=d#~WffcFEIs9`?!-Ze+uwbODrA}b2uNn_wq^l}2@ z*;S$q`DK|U8O14PjX`tKNFVW}Vq|KaoGkSYE8vlXFQ5~detBnA)h9S*n%=S-RrDK_ z{9L{)L8dK^+4n)v2sJ_xC$4SZaX5SxpU9o;P+Gm7|>WAn7>;PoAHn5arQd zQTQ!&^}))>THnV{hxm(j#+Bt3=j-#TM$;?56Mij>E!K;`-?ZCj+ zLCHlBiU_bWU&5PFx2q+br3ygj9K<__s;pIxnhuW|PHJ#=q6|u0gp&KS%?aJ!a*jRD zC^HCY_xplYJTn<>Z4KO^cok}bJEfM~O|n#&&)8Kc9x`oY0$j}sm_k_wSz(?=QLw_u z1c#x>u&CR3_$Ab0bTwazIRx&jA6}%~?|b;(P4y~r-}G=ab|Ik_RDPzeLL z>}_CoVhf|8=uxyN`(PUGr6q>=;S8OUVm0-8we#>{GiY1PBTyl0m5{*Z#REKLmhUHdH zpfeeM7xluICuFEDj;!pCB9b2wt>LafP>_Qm9MQNa+0O7G)!^ zuU=nKBXF2Z1J&QqW{k@4t60PZ?g7x56tH{kh9J^0RMll@t7t1WYxKJEeI`ypSlTJc z@yIu#PVU=l&D1HXQq&5qLRT<%scod|6B?V@^UU*y-wb9^2;8#;@j=hz-CUiHm0&E%GE&R@0R@BG3u>#gv-pqGA1~nJQ~1fr)y^m8 z0mojoh@XVo^0RDwW?9DU2f4y2ei#b*y@J%)9Z(H-p!`#L;LfGw>&w^}wa z=xDVKTZb$9iszu}XP-r(P)#QCFC%TLI{7R&jXgtogHT5-H~=+GvwE0^eZc(Gg-*q2 zVEjYP(AuHt=PmQlKnLM5fF_W{sl0hCd`2up!%altcp2snF;Qy7SEEjl9a#ALkix90 z?*+kI7j+(rn0m52CP<-ui^NaX5K{xxM=%d0NZ=6Lfr8T{?J9-7DA^SJ2Z`oFnAk*&@$=1muI83$~$uB{Wwzpp`RxJMaV5XQfS^1GD1 z7r4%KKTxAk5q1&+`s`WdN!}|s5cO+1AqEv-KA^g!~( z=x;$$&|)PD{MkT|f7(gyP(V60n2ce}OC?I6>he((_)^3I_rFXG!^^a_E+lH-?{ne; zrI4kQ5@X;0DKTovT!{DmJ{kU>mq0b6O%_~Vc?y8e@XMsjakOzUhk|kyCPRW_33=>c z6bmntxhf0*r5>ju;FJqt2&r%}19Qv^tT!TjK={l*rtxcFTZ@~+rx)>>1?&d*`#-1f z=MeB%sczQPsarMuTAIB=g}@*La)gkPj`Ljn0W}V>Dy{nB6m4#aWW0_ex|P}$eZgPK zF~^i-7|#0U2NoVv_DzgjYnvGP2#U~Jk9JOaNM7bn_QA8=MMZIK1DCDs?bpk|Ggel4 z#KFl8jM?Y&PF7`JsoWe9z?nVO7&p6DdNwm9E{rZpQ^0UYazS&>A8;~=4_>E&T{RD= zOd8stS;4iaw~0)=PjkC2vesb25I&nv-9q!PY*= z;32OoNKvco=;C58D1YsQM@y0s@U0fbK_@H|XrTZL0p}MLs1g@S;WFS4*5V;H96Ug7 z*fEENu>QPv!FfM0U%xp2>ims?HPz5wVy+q~tJjtn*X*-!3d3{4l269I8FSQoZ)3V4 zw<^7R+S1kxpJ5v-Wc^W4G-()M8FF;McfkH2sSX2~L=68XDri+U_Hl6@_N3aoe&=)J z!~mkR9$1K)eEWs6J>wUGkDR5dqORA#%N4Q$lkyoGUEF7pG=Dv;TH+jxmSjbw(KxT4azMjj z7hHgeB^wHwV4O98+5p71!#q*|hz}HX&!aHV#7W4+-&jekY}}O?MsYalWQf1tNsqIy zCt8y$(<_qx)p=!Tp_fR8H?78}kh7p1ojsRll;Je4(Q9v^bb!H3lg}wz?CXqVPm&;-bst*F} zG}J>m5aY~c@J|4D{e-;N!jo11enP<$H$XD+Ta$R*6n=OrczWI67V+Cl;|p*KKb>2j zu+v>U;r!W2Gxbl@dJpfL-E!dsUURUzzNFk@etP`y*ojZTk~T2dT~e$r((C;OIf-X? zdF(tR9M5i$e+1!PsIx((Y!WEzRn-ez97bILw#>mVOcn!T339YRXo7$Tk5R`)aPPiC z?MsU-h*MM_fGbg=aTMcMJ145}nl{B8XQxD-zY(PE-%{?$J5@x~a+7_Sy-B{$O?DkT zS$S^dMJ5s^LJ|K1m z3BN1}8V@n8tgR?A@_Oysl4?Ptp}KX1+o-6^xAQZmWnCUu2R?6HXuqR3>dW+HQN0{R zcCsoK?nV?ZQ)|)%z>g-1!5w27TMe;*Yn+SApE8+|zI(lnr&VT^M>GZpaQ=LtEAHyS z6mY4YJWVU!C@MXFE5IPxVjF zw2c@o;3O*#?cylX;)%TU1gdQocreU`K3#@>l%wo$}5g^a%6$0554tjOiQ{kw7s{#Yp7+wXwzy-D`-rw zbTKh!A|t&*Pj|<(hfmP#Ip+sV^p5RYH=Hn~6J+<($%{+VH>=+FMq);{w<^+g&dqe)jW zr)X6xOLuh@YrZ*npq$Bq9Qy|DzPy^1FZsio|=6~=@H>q<}X%cQy#t%z3i4=G!CU)R-K5I7IbP=lRH0Q+IBkwz&2d;16S z3)evL1RYgfk7kI+hB*S#MBN1QH_#vR{!_{e)c)oB%Jm6DOJMo4_5m3}H=FxTzJDbvj2l=Hrcl<9RpEJCms zzHc6Hm|NeK?S4Mn`8{ZaYwC(CY#oPdIOpVf>6yuAwjK9;#oIy#qkN{jv9_mg(Ay^M zIs4w}9WgA5HKtdjS0;ivZ{Tw4=d+)kxE+P*v*q^UN~4)wZGW$M$H=vr#^$TvMR#xr zF9-FY7%2K26yTMRa5C7hE72Y_P48G4VNRe(7or8ri^EQ8@6t^u+GX>B>a1Q_+rj-3 zKSK5XyM_lm0AL~p|LIHA;}R%9T@QFbFmICyHL7jU;qHSDS1nE8rmzuIH~?WX*UDuA zsB9h@cn}(IVE;t9AJ1lQgP>TEo<`d3J?LDXT9YBr0EQ*z-u0%UT#)w%Na0X6b#h@& ze4UEjnBe36X7gENia|y<1XiC4<{r(-@|9*m^TIDphP3^7T4kvLdqsD>zPwuCsIKZ9 z=62?_XLRw?`cfN%CpzZq$Lgk9z{*z)){?wAoU#B_{%Y)X$(amAgwq+ce2od(A0D{p z*xS1{c|Q|SD=p2gklyZI>b^4m^_8ia%YPX-Ja~G^=k~r|4*u;7U1=|`Ff+AzKO z+V-V@(U2=NTMIOw*6L6<^Y8`K1>}|u%0)#|BGK3_p-3p;QUrM`Uoo@JWKU=j8kJ1| zJ|s}xpv9|)LXRkOR$@pK0N@~;N20$-aMjy!{2TbMj5C={0n3Zq*(tn_h}?dh_#QgM z$a%s$IGvHp&oQsDG78pp02YYgq?RDpvX|Kj6adJA3wdv9BqiM+P$0W`&ok_!uMnY6 zb;8ZdeM7?^_Fbx*ZvF=ig~~mx0BPDpq_(^ABUZQoAXnE0ngBp*6yM|e-(Ls4iAY2$ z;#W~~Yi(om=wTa25x$AwzKAxl3YXL&8~La+~}nLZ1kO|3Q$fNjP%w9&!&^-2L_t@ zhVPUbfuS#_-%DY__a>eV2@W1iZjJ+AW4zOoAtE;D9%4k$W3QY>0YbGlM&N$T3OeFM z=;Qtf8n{HFf$M01bOUD~>IUq+g<$Wd4(`fd@g~OEvv;Kj`K`V8=mtnR-c0^qjZa;` zLonp-I7dN6xbih1Mg&va-i_|m6? z*N1+f*+eX6^20NOrKfz@|JeJ&o7?KZH(?Ua&NcV6&(R7Gl^Emx&B?P94#TUJ?5yB(YHp2P1>Vl?^Bsdax}#E&fD2N&?#CH%~V_3ur%k5Aw} z5O+O;;)EK)4JN21QQVbXRJby#APSlom?nD$xR6dT$G%LZjP9=usegB>ffMt2YRl=5Y3D-K=AvnK8| zcjYmVUde#Dgv!#PvRvXiAETz#4hT_n-#>vLyTE{TAljRIcOxaqD~JdW*Oi{)mrHb+ zdRSx6Hz@0PZH-#JSB|}os2!>v7au= z&V>v*Rb~zVGa@f7)Ybj$^UP^b1-r}?jjbTo!tbH&yS4_ie+o6HmgX1}@W#1srtbj# z^ai@}8h-dayv4h`uA;ie#6I1@qWPoOX`pzATa`eiz^o*RRe~Y(K~!z6s4+F{sp8~_ za#F=nv~wMMi%Akk#{0&xkL+My^kIZd3h+3U?1_L0Qul;WH+34W;QTK#9k9WYtkoTTRuG3m*oq~HYh{sZj&;q2>) zHFEtqU^S0);wNYEx^J67%TcO_f(I12iWn1)QqNG^805>pM#; z?ZY;9b@QgNw>;lEdHT6SO=6AGnE3e=l(Dzz`L`!eeSfsFL9MB4-!ig)(d*Nm>kS`R zzB110J4!yI`>IJ-%05%fh$*spU5y%(Eus1CK{$*K4S&=%?w&W z;4zcb=>gm%b{!}~AdJLJmVJpjEtH!ir$PbLmjHmTvk?;6%+e7k5)3k8LYp?$Y5<9v zL_H<~3PO7-NrU1c1=E!T4lT5~I#w5rcIG65*ReEcj*hJyW8DAe37qvDO3g2nh@_h0 zv-~oNI!^~(P(4gnyc-tdgaCg7AH`0Hu>_$Ku(X3HCZ61{8n8_OVRaeQ8R3B8gBa1S zYMX#W^0lAgtam6j3v-#9IO|=??c0g#!%IVpLyIH$e&F-Z%wp%l7qc!(yBz``Eb%q* z=8#DnC#@m9F1dl8M17#4xQE`kR>@b90Tc+;tw*e!^uSD!Ff`6zBK8llWL9KXC--!> z_Oy@F&ZtN2(Z)ne=tM0iwIS1-)I|S=dSSdak*_WkDWNr%p{jb9HpR*jZ4TOh$g?)9 zEUzZ5rN!cCI!`yQu*^Ox7%(^~xxE{dja}TUcr!Hs<*SPZhd~c2$j#T=(KyWL^#X-W zWuGBB^AV6bG=uz5t~$wE*^2y=)Ec2cBT^I8{{VGTB~?iHS`b!@1C*=^P!%a8H0Uo* z!CLLA9NL-z^Te;AlnAlJKRGHbDSunKCAloCQhK9i+S*&+Q(xC&8lw#pjMfg(4V^R& z--B4E^BKmeJi4NU1@V6$kHF7v$1XW*CqO*08UUPEo%b=lF&)t@VTmVWJj2gK1;obs zgjmv`Vwe8;i;;z~pQirNjvuPVd_BYxYpZC5oGkt?A;VZ{Pw_CEL^6IFU|CiGVh1(q z0Te7E5H=LHF5PV_%J)7Op6)ApG3MBT9i$a7%9cKEUuyjcs%VtGDbrK7Pt+`Q1a;EO zT=p?0EnX}-dM2eeyDf7Tzi;B`yRB+7Y?7ph9QynVvF9?C{soV-hvfO~3d^~P$MJK< zZe6X~26-47*l3HFznG9GSNIk_&OV=B*s7{79DN+WSKX(xYmAW73>&o+3ksH`dBVa_ zc**uOB}5MwRXtt;d5+!TW)Y+O?-4S*2`*|FN7-AZEkWoe=`j6O)?Ycfl%PZD~ zs`H9{AAdhjst~I(;GN+Wxjg;Rx?-!gQ}_7CCB;EHaY^Lk>;qZR3b`r;UZKb<%H)+C ztSfHOw?m!th@om%MXck*<4^B>CWw73z9zXe%OJ+<=|!JuUY)I`(I}{H zFY7G(II%KcCozNV_FS5`lqd8Xx*8^EYiM$`v|DXpPXq~HgtM0!ll4djBf1ATr#fQI zr|dwBjZfp(r~d}f@JovrEjSND&+Bwf<8dbYNKRaGWO#?DEwj<t_K`WSWcF)n))MYv=uP?mxR5sK#%VMulSe)es2CPS?OxwTZC4QuzGs05I` zK|R$_OSCiZ;AlpPP6;*>&SWdQ6LWeDczu^PjiJ{k*`ECr$Jp5L2lv>RXf0- z160fP%9gqtr4DUbi3KR9yff!wrN=^8>ck8C-ZQ0?Wf~F(=B=%DW3=K(QQm2;Tj~r} z%;2=?)z0umJVI3X&Nsm3Qmr>=+O+sUb)Oz@w$;SwbIc(#ZJeX{4{>8};Rn4%oasH* zxK=NAU7AeEK9dFFPs@mPz5%kz%56K$oV8gBQN{(8cAz)v@FEN#K zr31xn`et>R-n6@fqb?{aRB*#{52d`t4@PIDgwO;K-A;5Sttf$AE8ZHh`Fe0;@<^Ef zd#6usI%3W+WEhjazC1KO`bB38&bBZJZ+&}fz?C|tD9K3`#FCJUKw-(nY z?sV6!!okZAGsjS@>r274uXg~6Jj~eH%!nl8^|E zaMWK>m!<{iOtP7UCN`Xld$bI3d)C980NYy-R%<8qerqQ7!LJVJO%41ei=nOjcCtRX zED1isI*7ty!9}*Vg05$9$(bC&JmM!q_cR+FhJ|?Cu+kud6(CPuO{B z8sG9J)Q`#nyDzo7DpYU>07eXVOgnIy1f+$31S>vt4e$>Z%nZH-+L~JkkXDe_+=4x> zZXq|1Ijoz-r)GYgT}K4+mpv}yj~3U9-S2`zK9XTAp#35yMv$o_$3fy=qEZU?%4PiI zvN!wWWkPw>)^TTGFAp2IrVFFvh?1ti2^8{mfGNC7sBH$;K%5f$baG7`l^E9@R zUbdf=AX!dcZ1~ZV(lCC7NSC3N&;^4%${ya$dg}s!(O|6Y174!h1@6R=3X?Z>LrrkWJJ34wYJYjbhqdkz{kffoZ`++-Zu9Lx! zv1;})qOW?4ZGb0EX&`g@>e>F zZQuY78Y%Xr*{vRo)4^_7aUjpN@Z^nuKiL9L-0+tZ_+=2;Rl>6K$0h6qZ+#oKw3IO` z-%z>U>B=OYWsgx`rL({~$ZP!6vIOhe13)K7!v}$;u1?v0p1bTvHRy_zJw%ozbt$*5 zq4-2nRzNLlx2YYx>yKhDU_iZyM%Rmd8J_r%Cy$|>N#VKM(`l!>^CA-o!XgPwjAm__ zx>;Sv`ggK!AF_`ge*LiL%O`45D^e;Ye_mXizwq_^ris(P?yhXCG*{Ko&n|gRp7{JT z{HJmJq8;zhT6FcAa$1onQ6=!yo{X5?7R9cCjZx!g66fSsQ!br5pY>PT&36H7=I}_&FMbx`E;5jTN(8ZWS7*Nqf>xVYj7ze1?X z)Qai+iCINA%waW)0-T1ssR0;v_YBM~TXr|t6AI9I7cmOZsW6@tumzyYzz^0yEWld> z-gBHYl1)#(_>;TtiXJ~+L%T&nJ9K08i;>SpQzo0o*WnYh z__1lcc^dDTUe5-*Q+L3Ia}}Vvl7k z)f6GhT~N`cQvNO-3yh~WRnsUJtEu85gUZY=DpMJ2xT-b?E^W0Ot%+>)sj#UvwT*$3 z$sK3Er0Wr6rYp?kl7rwpjwi(1P+e(#c~hmi6q28d!$vtd=gt+TjK`YSimOn&G>NiAjJhMy6U*y?yXd6(=)1 zT@)KcvvF{!sA%|ueKnjsS*9dSa%$`GrIu$`CXII4>pFUAMVu-Q%CvVWF`V^u zIO`Qfojmvd!s1=*IPwCfmQ0E|Xma4Z7on(_aIueQN7#Pp3n`}L@{G!a8|FpZOvBBt z>7Mx|S_O<#Di^Z!@^ZMU?4sN}?z>d9Dz!icvZ(@#sxq9soDA5a`(4(E zJidVQnh)nSe}rN4Kf?KFWbM62H~;A`ram6IJ#uU03iPY@%+4BEg#IVn83O1!XqZC5 zX-_ofx~6*4yp8RLQw1_dQW}ZX>0nyePl;4n3K3uHaH;L$cCZ;J3>GbyAjstl!x>v3 zaV*qz3~IZ^MxqCL--Y}(5iLZ#3z_}s3;0HTF3|)mNiZ;Bk=1{UGhM7R7)R`?D$uU( zr|Q*J0DvhHWrUN+x`MBxemD&-eGtqYFbJ=Ijy^8=u>5@KSJh1rITtj;iOfjIJDfu& zf+ol>jeVV0l#@~<whJExHMb2^A3F%-kpRXtF%=7fTM7Th{4)PIF*_=4;37@}RG$!vbEJ##;b;h28kBF0FB1b$-XJ9H!2?@M+h2WpCv=591K32QYIN@+uV(2}bZ= zP#aWHWd9TZ2ce4Ez`jGBfg(Wal7zRwa!|30NqapjNODMiCLN^XQ;PM z+AkZ1EI-p&UCQ*bTbg&?U>`79t12yq-Tx9^j=&q}pRgBmd|08~I2{qjnTzU;8~3B{ z*8I#|IouL4xqb9J$L~VG=%EGLYvl-|vlAiGC0NRRe2(}BB()yo)KpcJRg_KkSxx=@ zO#v;!zGPZ7Kbenr^Y9~o*XaKxD25%E42^W=3+x~A+5J5BA)zIcvH&K9pv+q5583afPJ<`oxmTV^&TOyXx?kmv>lG zD>F*cJKIW1yM|~5BnQD-^9~iKN{4t|t=$gPkMobjrvS(hOj#l^nZ~w!gC9iw4MfK} z3v^Hd=}{i|lpF&Dsg4{2eStr3W$mLkvCq2{>*LE{pZCC6<+~R9yaA72u`vwXD+5pl zHbWI#001&jlphw8^boKG??&MHO^yez!#ow7a8JHX@(tjYpL!LAD@6s71f6eHw)AjU zHZJR!OHpl-Y47e)&dK3;+rA$9Wj2$!6x%78_w5?zc%F~49c-hajM>WU-L_%-(YRhu zj=35F%PTIAjSnwgg^vEw$zR8Q82e%3pEIj}p7{{|`(zFe&XZ(;<&6+;og0%Cl^R8# zB$`H|mk_IoTJIAoQYBJ|T`h74547kVu+oA?1nND|qJKj5s%y#Q;OhL?N06bRs<#4Q zq=(Uv%XkOnugnFIO(SopsFDd$RBHihT+IS(KKKedh)%El-^XX*=arD<2N=i?fW1Ok zCB?4EdnS7uKWBehK>W$tbd9n>NKa$R2J!q5&Zhw_ycO>2i}4JIQc^v+D+<62C5m|H z8)5#choV?X%@>)$TXM0HY@wKVfV;9n?NvzuR&>3j_Bz+OLS1(`bMVExBIfGu^ReIi zRagyXW7V5ofz6)tG&f3FJsh;w>W+)|4xUu()bu+^_i?%&LaMa zoZMVL-dV5W($j+S1Z}o9md;x>>ICbJ|eRg?9^}>f8Q%x7jYQXtf9M{QFBoUu%=vSymT?5-# zKyLx+x3k>00x6H2B2ch`rODDxs?0qo^o`Kt2M>68MT>U(o04^TM)71_vN7_jk6joZYpF>Z0oSu3AHR zOC1gJR#;@4c`KV&&M=|M2-lZZG%Gv0xy!!5aL2qJf7Ev?ePw&C$IINnjI;4 z6NM#2B`5ob+fsE3Yr>tIts@h7J?(b#0x-=G0TjGE$LNF_P&M4$P9^6SM1=+ED-V-) zAwwHS7tHi1dwHMNnl1>I_dtS26D&MH`R6iP*g->=)$&}`X|-yIeXMKO9TwGCjFk@4 z{x3pqg#R4zm$a#Kv-HaOd`1zLH&(?X$ym1BNeYT+VJlG2q;cg6MUaB0+F~0qUaeSYx#b!TpTW z4$%zIW;*Q~;1PD^ors&l+zH2|d>Xcb@|j-uDM#$wIO~x_dGBHP#n$zyMHqjg5gP z?zTJld<6)KLV~+VLkI;~FbZQ6h@erAg5)l%s7#%5iaX!V z;3demUP3F+vlo~PmzoE9zwvT#&b|xowxhI@mNWS`^M+C{dFF6xcIiXa+i5&h&B$+O z_KGi^$lx^Ws|+&ir17s-#_v|#d9Ta9LeA9wX8-;zG;G7>eDUIH=ke=g_karLaRwKo zeyDw$A+)ei7)yprF%EHId2Rg7B!?vq)Ph?8BJ>4iEy*|5cXg67`c+c>7IZ;&abZjx z>6`&bJ35OEqaRFM8WB#9jC=r{(*e*o&f%};)+bMZ=G%D%mofhDZQRY$K31SdD&%1&_{7nB9CHsj(qC-y}^$B^yuQsJLuS(WqHyLV8olVozO`V+s_Wt@M8nC|i@(ySF z(#JFN&V})!BekXef=a1A6;Lx}bE~qJcg0@b0Xb6bCX7n)gPI=L^xXhEMI{miKn(fPL_Ib(SH$GUZ0!X!qba z&9>p|OekpBNuC5m<~i!Ydf3l)qpguSg%Rh>dpTwL24i)}oa4LE+0MdwPJ|>qH#QY+ z(>kgFzt9rktW)o3+V;UuVKIBZm{^;1PYVN4rcX7z^rZs z=_xcNWaJuA5Y!G3)ge@8Lgn@-X~IcKVrzgVT!f1l{PiyOYjkEU{(1*Iy5aIE=aW!E zM$E5q+VDkY1e~|TC$b+w7*b9Q_YB)f`AGilpdhvYoQ+<8hW(Fs*_XW`PAaur@=fnY z{quukT^;SS7indTN|eY2p}4;)rGR9k6gX7X(4V_ewW@k$7k|vNUOW|Y^VkO=cvYaG zzRXx-W2x>P-LG8?X-=q(=yP?BAjE3{=&wL11~10TPG5$YD_|j)pq6f9jlsCz!in_` z^E>J96L2DWd$cvBEW14AR@Z3jh1pS`R^ezA``|(LA>gPb)w1I35)hI-*0^B)YyY^d zvFg12ytp;BJME{iTQcmPSmr3HsIFz-wzK!Qv42>&bbhq|#>aGVsYb2W(<@J;F?bA} z@C=JR7Ut0z({OSY=H~86ymsdFEZz#$Oy#WSlJnHX^-J!Xop)h#Ne~ud#`>}O^EO9? zx~NQhdYU6ikz|Kt&sqy1<5$qRd;Np!d2hbVlf)ZT%6P10G>paj&^#ZnFC-M_%B8d4`25}-&PqjiO zr$Yp3JlbD&{G-asUO!{x=V8QPk%5z^}+t^fw=&59`V$c;{_1mypuEb{n(Fy8Kf>cH^Iu8zUqQ0qHH0&0W|Tk$YeLz z>iYK^Iq9LPXOrUTS5O#KPKm_F56jz%8&!DRj|i{w3yN}hhJz+8xKJU4cbgIKv*L#> z>~<*Qc5Pw0u7@&%onj#4X|a0uhv^xDcM z_z^$NtZe$xxe2+Y%`ESkS^e#WPX9}%B%GmxwZV=Z^a`~y#LO)p z@PNLpLDjIxUA~{1fO)I?-`Qb*{{Ll%-CaW+Rf%9~xVAjNEFZYL=k5WRg^UxpYTOTQ zD94O@=YM%Y&|m>TL@=L4BSUvdgJHxl0^%Cu{whx45&65KUD2;b9X)l-mQ|i-N^b3| zcGL}9*@qjpPHsH1>Ahp@gMd0$x&)+a=5JkEy0y6F>ap*93=O5mYRfC52WO6b)-hW< zX})5)QZicdH+rmsj6C*vJ)`hYCgnwCY)RZ5x!>n633p^z06VzAmPa(ZWu<`$XiP5) zwswyh>uW!v&8W?!DzuOVA|;i$n+~CJun-zrj~h8zVfh(Z68a4UhD}`x(JGor)XoBu z1ZAbf1l*3V&mKtf^xqfrs-#K|`7AR0P~Sr9;_P5zP4-|6?E@0JRIfB)bZ~@8UUJfE z?=G*ZZlK*gK6b8QFc%P&8gPO^Se8_slE4Mn`B7IeR}w){tt7MVdz2iXpP!Ky78EJk z6XH?>B>jA?yJzexS|L(p%eh`u21MLt@*%v*K+=h(DU+&F(ZFx_bcp7Htj%h5P4&s9 z@Md8r{WEHVa1;}uc8b(=_?{~WzrnNynqsXdK`bI2Pic-Bq45IuDJ^J@1l`2$}zg!-0Qp0smf`YeEzfvGi*o0qxYZ z-QyZ#HD>{z`UCh}OX~F|y}fc;t<{$7GjfQ*K0j8<(ovg_Z|MLtfF<+%RZ~|3Z0>X) zdxmNJ-T=A9t9;5h1Nn>A$bI>pk*H%0y^!~c)3~Bb{L(GB5Wft_-O~gE0xXiWE=Sz zUhKjf9+h3W}rw`LI(@+Io+jZWB)M&CoeG7 zFTq!QS{xk`VM{ALSDEH$G}P3!*-q7mbO#>u_KMpobxGI~>93k6nwREgPtHW$i3XK< zMUCwphj)|b=-7f|%<&c7RYg*FSb?6A?@I~^IUUw79Fi=;1}++nj%GLAeS%@Pdl{I|W{a97Z?>*Uh_HuY-ZE;C;^$|yC zeONzz2@M)m$|`}_pKlnC$0k@0uzV`WG|;H*^bZ6ahC}EC61= z_);e!`7X|teAfn!h-Cp9a!nYJd>0)D9yPWKe-3%1gMb*@iDgw14~0DIhr4h(>W3n; z6?O$HJR1uV@RiSzKU}Uj!7i`_w}G1tcObW0!%OF9&d&^wjmW1O2Iq#?;nQ>Y#aaB+ zED(_E*{kjk{epjUrh=8V9(qLQE2yVdRp1I?5+@L;&4~+8!jp`mT9P#Yoh6yA=0NfZ zJCebGf)f!hAyU3=z+zkAI>Ok2{FGQIo_@cM5O)A69B>_<^#K1g$zGHSAnw5Ca#%lf z7{uo!v_dZEpxl4^MUhP4kXT^jn#&t1>=hbysm`d69Oq=`<`yL79*El|e>JOGraMY7e#e{)=76MykGno=f^NF-r1CI`3}q0} z83VYE%RA8wqPt?MSGlMd=G*s}Vmi-MSO}PPaz;2J`Iv4*jTVwz<~+V~m}#>S(-|oB z2vv6gdSf#%5z2e1O*1GoQLT{*Ac{WqG{}*tP*t2FnO~`HQV*+X5x&IW}OLxlk5IIi#N^SyYuURxU>F|RqothmVa`-2-@`&QscECp$mjP zL|L_!u(8t^OE&~x-M8EG{jg`EL9CQjDfw65&A}^kA0E9Fg&*<<0<5aK7Lupi_qOku zT38yr-tmc{9?Tf!^t&=9{GAv-PtOAv;`^g+(<^73Jn%@@5*q~%wSY(3)n+ET7_LHRcfU)8J zt(e5H-1A63iQ38tMgy;cm?oGn;Z>=`BfYfZ$4uf)p)lz1p0VoVxpkQN#q{P3rq{;C;KGC zor*T)RArjvy)BknYgg-hG5&-T*?Vk@Nbs48D) z;mq-Nm$p+8V4S-gWu*xjxk=*0j9hs#+J@u zywV~lt_!pWb;7gmVbI zvk=_6p}U~TV5O@m>dpTmq97$dOefis<((SN1|KzsDeyb9d{ch^2@U%Zpr}Isew}UY{vq~{XF!)eceKubblXUu z(Ed_T)BcO>;l(|ivZ`EW6wd*wbm*{~uX);8imdc^V zg{>G&{U1d~9J z!J{)_+zD3X`#_T~Pge!AIq3vE3SYGvAk zNnb6HIm=C*llQPZzGVvLy2F}6>imu#v-5q=y&}FKRUI|k*r@L|-k6%d-j57r<7XA0 z`51*cQQ=@>yvu-Bp@culP@vB%-d7^b@=gzqIpgu3^{$Q9<%w!sG7~_3V98a*QFfq z2b4wwgAf{=k#aC5k4ii;T`gU3s(GkEYoPF)7AD<0)-lG5c!OAMF0L*rEh}fYcb@EZob~I8X^EU?+ej=ohu{HXQH|E@6(5(?jAoY)NLs|s(24v zo?0&Ii|9%3+ct^9)@^$0vh^&vW8j>=4(i+SjbniD!WN_B!0YUCe-Im{$y!L$+#pT6 zYjQJvK+*7?X)^r#dNgMS{Db>U|;Gu8Js3RfqpHbZ7k5rK@vY{X;jVHV-)5K78xQ-IITFWlL&{%XF0=HLe@lG`sG+ z&GboRn3xWYem;706n_f!!F@$ugY8o*uN42TAkV9AqUw1We2y4|A7kuZC#e83<5dB9 z9=a;>j;1<96W{7G+#%d^+pTBa4QGF0TVQcoZ3J(Vn49}z_Jy7v^>P!JJ!?R(bL`1&0bo2?$Ga>OAN)O`bvK3tpgX1ee(sS{Jh;jHs`e| ztMi)?Iz$C##mBfayn_e-=>C^%$WlZQN0X!Mszoj5KNQe!^Kr-hdh&#Z{7Kc7D^u06 zlJID$KlSW*;hoX6XlgfssyBg&_`Sb((hW|ep=Zg?`QJxcW=2L- zQ+XrPvt6Srq8nE{vfE<*e-{1<)Mx=RZ7Cc6Al=O2M983Q80_6<&0u34MgMGO4sBVz zj~ASQQZiW``B_I@*=WZo^5yE4WlFozZI6Gbz1Fka!=i*B`DviG>s8JUyfxw>;dc~(@g+dx{Qzxhe zxlKohJ#mQ;yoUS$t~50;n#Zr`Cpt%kCx<;64s@~PalV}kd@bU*$Ie*aZ#*+|L|;`@Sgt-g7S!f>gC%hl z9Ez!$5gH6IgfF*_u16tq8bD(ORY(w~BmDJP&UaIY_udnf>|P#y>AbPG;|tc54PM8T z+z8nmt_L4^fP?6dw*`@zIj$jo3YC{7xWdUeiAbsjkgfGqg=Wrb`Qd3InbDlfUf*f|A7SqHLi48p%-V?=()3~)sQ*SzP{Q!o%>7(*zkI3Nh-U>EZnR0gwI zy^X-peNwKOx8lEA5Oclo+#H=xA9JRihLW&}p-O#A8>S9$Ge0m} zgC5Y>DKfn%n7`dV)Bind+RpU0NiZOV4m*Of#};2?3UsPsrCQUb$SY3 zlJ~PWXWBfsIXx%CUmO#x%n!6G3Rfg5F*2$Hs7-vnO!sAxRsKvNc~|~5iz$^%V;P$k zlkSnkzl)gW_XX`pcB!Y|cCb7Io#*`mezpBF*tNP54^c;jiorDTg|`%hqy?u1W`t%x zSGu>XP^}u*Jy)K$Z1E96s4BiV)l!2U_f+CsmvA!L-j!c=?Ctm8DvwoUR|F1Cm*{$KusQZpG|iWjUq;Ef(Iw=n=_WT3 z&=Jxc#6P~7f7G5A`j$l($LB{Aw&PCQy|LfsfBo$CR~3~7g>#?yfp8OH_Gg@6wB<^b zx)chD+`{5r<-(+pm@psj4Ii#O$Uk8RY<99bnmj&zd*U8+DhB587FV~Aj+|-oHP|+> zCO^J{JFvmQaob6o-pI1!t@hr+dEcgp4j=x>o&4HU{9BRwP-O;U5swf2)=rp<lYblg z`yF=qi5XM#loA(7(K(Y14n0CrwD1bYKgM{O1&7r3RY?z5o=oP;37bNex#2oVmeJ}KaEJbMhBC=0dSvyo5q1cN zsV4MYm7xkB7m+90b553gJ#)%m4mI2sfvUbvj$q4bCYFEm%&IN#9^Aguk$=X%A_|{x z$j^NfE$1fx@%M}5=~}Y-Iv+>=3~E{=|D@JfP^?E%8#Ax^oM{qMBWL1)7R z!Z1ZLIGB>AR%xyHa;<*Yip&eft2B9oRmgC#i-DumEY8B+^oDti42c=MsaP$ zMPM)r^K~$TX1bgWh&=4hKj!$+whuIkrP0N)y;Hiz`YATYP8w$~B2l2IOd*S}2O{An zS`vkZL9w$KXtHH`<*gXbgA5)b_k%L)h~StsI2$;spt0G3bA7XYQ~lE;Gl;+h5PSC2 z{6A;%Cdr{m!p+KNSNun_;t@}a|BRSY!F|XKmS$u|vix<%6;(hGo!d6mLdb-W^7IHv zMp`I)gtm?3fmZThnV+kO-vF#zh*`#Flnu~)*a2A5aS`avDQX*V5j0c!bVLx^NPGgL zZqAkESFvRJp}hbVOArmK0Te$UYLEOT77U}=YI>M^kf}3>Or3Jd?TzrlWwdyKu@9h- zYAESrgG5d^v&H5_S<0*GWK-xQ4|4pzyJR0j(${hPp}PQTO|IkMEm|j7OyM=0Y3GT> zos8>J)6QcL9}nZv;y>H@4KHxyOUU)8BHz3Ug@K>)oxt17NxIy-@VdE$LYXjBEa(aX zP2DV!11C})k}Ff-vSjgxzEcvA}1rf zT(F$n2F>*L_@^+z#o@$rf8kqln`_6M?LoQ^-2fH`zcB53NE(K7Fgk>axpv*gYi(xw z4qXGN?0Rw6rzUrEZ_%thGLK*vw=xbW-tmbC%MwnC`1vgY8rT{^)iUOMy;~4r^GtY# z#9IPhp-xq#C{+@tp>JxwYmv+e_k`NUzX|nG19@7|wW0U;r4!$MM3&f)H{FWs3d=N= zJX8H@_dByAM$lTOXpVwhQBn+@gUY&97~+25*b(;wH~P)!*JIye zVf@QQvV6w$;taLIkb_91Ivq+ApirD40SXnt7?bXo8Jp_EhA}WADnMKdm-%ByfIv^` zM=#f=S{aCIgUlopo6R)##5gJ<6=)k7DN-jeND7y&JELxs7ZpR1HDm@mnuYlRW)egz zqteU34#{hf7br^h<_o3a*&!KN6jk(!ckXzT*UX3Lk{7Yg;NUn?)aY#GR~c8C1j~S(U?%{ob67 z;@wV)0yirXPEHECYNT0abp{Bj=vbX3>&Y&r7i+dTn=6!Zv=-kpyO7O-{DvC3zV2Zx z{zwD~pEocb;5Z=@E0#jm4r2-(hZXK3d12uw|AIMbOy3|nhZ2_#5OV^8DR``Jh^ds9 zo0D@94UrHjT7F;#QEG3FL74E#=*3CzkuQ-GWIab^rhlCy&(E&dbj2d_zUf~r+@UJ> zrqkyND)RLCmHD-bf;{a(Eq1t+)6otA+x?x6Sy!f(q-o=aMq9>=mzvzGBI{#V*Ykgm zBiqDk+Yz550xZmJU`CnU%i*Cg z@iMZ(P^FuAh-4j8ekbIrz>rA3ffz4-!c8>bBVcf40RE`b3*VSjTzei zIs+PV)4VscGbr?|XGZycVM$(R+2uNx@4$%QepGs9Frt`Y2!QH2K_1EGw9?|`lS(Z> z{+|mv$|{OFDp*_0KXfuMf%h|DI;`Y6o1~H|tDH7z1+op-;f|@IA2i})aFVWOJn)9W zREdxUfZX;9!fJvg!ItvsI&_3W2pZ;Jujjv}+yB?=@VMM|u6>f^OpwqSn~7)d%@e!1 z6~Pw4WY@w+Trd+KOp$n4m?FW9Rw4$}8};y|Z;0KYi^wULGy`OnhhJYq>7>C@iLKQ^ zM%P=?rkmPbB0PI4cpH{xB` zfRv=O-eMnDmHMbvakw&Gj?(met)xvnV=V92TeW^xLPUk87i4X@1&VSuCnkg*hq4$= zM`2ZIXMTmUSdj-Xkt8b3JITxY6~}c)50<5t$Ct#9UaIeJz5=Z9;?vxzf1kN{_!Ij; z;q;#N!;}0|>~fcx*@>pUANm?5QwP2o`T?M}BlF+S+#Z`BogVvYX2trSEMgW-e=~i= zZFd)Vni14+&L#wl)?#0eltf!jA;pZTMbHvvMcUlLF3}kFNON!}`glHK-;X~rXGhgi zaRFFLp5kpxtE>oL*D*TeO7RozX1p=o4~1kvQA=Ulg**~6wy)*4GvskoH#a0Jg1rS- zR+uaqXPLc>lR46c%1mm}BJE>JLrMeGzN|ZQ1g#OAc>-lI*aI&L93@_8ja;E$QwLa~ zk7C6Cnjkh4?r0x{GKgMoX^JpbT5kZm=<6~yoYnurdR0r;D;T@%X~tDcNqtEpJ3x6R z54yRb!BpVYf-~gV`lRLrBTM|56yKzP#1Qt6N0A41?pWLDiOpDBPu%ox-*bVr$XPtP zcmzYJGW=oZV7WVf{G_`pwcIHb3OS$$>cV`PY^GmZIyeO5T@Z|Sq0>_tWQ&;mvy^-sMC@AcDs6zbowiX>w}(=kZ_0 zu`!(*d)DMUOIDp*5o_@zsb4t8Re5Oq%N(ac02@d##55quro(KBKpzpSAiUi;)`FN^WL&+oI#TaARV9pTO{$J;VG#`8ZKS#| z{1g;s_$dKQMMqvyp$>}N(5-F)S~T^&{r=X|8=Bvtajx4F_*D_OxjL8``auz zKKtbiX`Wee`~Heo9&m#BD$flKT+w;{M>^ zz|8Rnx2l$Q-7T4BxGc?nV_hGOY3_nV8Y$Po3G}^*7%KP%u?K`wPtmLqabOrV9cQld zOga-WZS=jqe&fg2SuE(}$_He+lN_R$ z$F?wOubjvZ6rYWkA;-34uTEJ2t;lm^YuT?Q76 z745{UFa64NoBQD?p$y<6u#g*-0pTVAS;ABN(mZfjgZMwFpfx)-oa!!~y3I@%4HUH( zw-zFiUEww&+~>zz2k;-}l!YnJD&s!MYg1eYCpls1PJYD=0Xe#o2zSDFcHD*A!mqgX z;Ds%C!6I|aw1%ALWX-wFvTH1@sC1~PRH3r?-6HSYU?7tHnbR27nU$|~G<>LrIm@4l z;-7GNE$~2y*NNkmDOE7we)((ntwzFD-cf&_PhO!GQ9de(w~6}DGAJW8!&@8@tSqv( z%8!I*DvHhOY0aq>Pt=yQmRi+bD1j<~OvulH>s`s_gi_|@@>9^PO%U4&_!h_`(@TE4 zNUY|FcAi+Fa!X15tp$V>e*_zY|Jx|38x$?bN!Y6P>q#2c?0TD-sOe!V}u?6OlL{zkh&k%S5YZPjE;Txi3s&q7PP*O%u z^877=onMO>7_Ax16E=Ql{QB@`BOhan-E^M3KTA62C37p1Q&%k6Zn2oU5JeWakN7_X z9=JWtw19eZzTY3b0}}%$9<+L|S8^vE$Qd59uyPzHW6Q!rOM%YKBVS6-FXz*4E?i<;A9O&S%rMO>3=nurH}WH|}a17_Wj7Z0tIV zYTI;%zsz;QD~1{$ag(@_T3l!ZP{I292<+Ur|>p`&(ggc7- zx*g>Q)|6j2;gLMQ7{b|K3~e|$;^ZwHcWCtP*~czEX5bvm%o%w8j1(bd+92qjs%SKH z9&Zz-1f)d71leyrdSttuHUZeJ@PXFa+WJmb;wlaF5G}WzcA6%u#(o~VKIMzL>+v}f zHuJ;WiuI`$0jV>{9V}54$yyaZ=aHASH`OJPicC%5SfkMK>!3l0i_Q_kI!|!Sop+!0 z%wCgG(>3r_ks+^CRa{uQM=wlqNsI7581J7J8;=7*a4z_mx#>}|B(Z6ZEaAFk4~Mp4 zatahk6&$ZlBQFuILsd~`S*9;+&;K$iFCJQ#?Dw+564KbSNkNi##A{O(byoKTzc+r; zc%!Qm&0lSS+nCVWX_ft&5O#6p(jkcDD`;~lS{94dMT6)2T^pLUKUnW-*<|Mp=a`GxC8lcv5gfUtctBXwiJy(Od02 zD%@lHhTF?w>|!SG!nq)>BGjymq*iM33<-nY9Q5nB%UflO8k8B zzqLi6d~Xqy16l|k)r0Z=F2)<}6AbG@Wg*_8xh9T;y~&4BD+keTKnNrq_#js)i1vp08bRK!NSQO zJ-U6{VYk;#S4LBE@6RI_2Cv;@Z*D*D^y|r@s-jZ8Vb###o;_Elul0V`az9@uFHjY+ zJHj~MH6eDk`;J}nIv;qKwW#M8A${TF1RDaQGQ6|U%q=Y_Q8g6T6qV%}Ak`}qM5V*` zg(iz1d^(C`_;e(7wK#v=s4gG1LPllFdUe%MgfME4YYG7+BgPKOjBeu6>=Dqd&69@U%T`D3# zO^q7S>kE4-*xW=}bc_fsDnBS@!!0k2n-Wi604L`W8nubus~nAn0$G)1TYO&5oq(e1 zf)W_v*7foh=huG8LgqjT)ien*3(0jj5j@2-50y7m4;`%%MjVUv@rFX_Dda`oDI_a( zH#9}^e3=p@vCwsjf7+c-4dLT+pNo#pm3df|#HvyXL+{J}q537C<&P~s$>k-Qs+5*mO7n9S(!5(+M!m@+C-5g_;(uK9 zw&RG$E4&9*AS|u~LIOM|apv--n&Vlw@M3+{;1d$)Rr3OhYZ5*u7uUi}7FA}^B< zt5V;Iw6{BDNGQlGi=MhzHD5vmJxWDxgA)0&A$a=WhxS6X2utwx(od{e8Qw1PFhFMrvfz%_jt@{5hAQ!uiQ_s3|) zhAAw^m_lc*5oQFWM)xCNdz zMjC>9S&Q@aL_{PUzf=$}&nU>WOe-mooELZ1<(HOPX-ZWF#e9S+O%Yqn+T=u?&9ux$ z2y!B&gG()CY3W#u<8$O{tG@{hpxBgJN)l+!_p3}O#D}p~II*WBh@3J5lOipXW1?b1 zQ_GVJ&#Ge$fO%iYx4*#~;yfzxu)T6i)N5nrNl5aG2=;FQkDAbl?Z@5L=wL zbtA8V?Ll!AIi57GWif#9K{Tz0l`>cs=_{IT;K++G$-T%B2eS45`+N;vUjw@8lPq>1$~ikSQVkgtq_eSMam-auoi4SKJNa|yB;|C~=HwDo=!lC9y2Z1#!sAFU&=qHLrs zJ2qQXS*^sWYq;*p@~Eqmisfy*#`;P&cy8Q!~ zMTq{jM`^h1Q^CeJ=wdvfX%CKHSf%PBJCj%*5=Mm^t4DaleY8-rq2@PQ5~SP#4#_p9 zR3{_IuO*eb^MV3FKVlmGK6FQc6?G*Wg=y&NV)DLQEfb(7t*92UWrvA6(Y*t*dlH5N zqX3Wp+VNM9#4}@ZEx{Qo>46-u{=w+cGGwy&LwiVUhTC1z+R3k>g8$FM6=y-!1kK#( zxp^nUP!=fCWI(+s_~O7Y`A$$68l)jVmRL_LG8t?$a)uJ-8P{IV{SK|g(Jf@y49OMI zK${{oSO+TN##>lVJ7g~xDf5qnE87fAXd(r)Owf7h)@Lox(RB1;O!Zl~s(iVxahW*H z)v#aujA#Pz{C@7yS;Cbvao5atUd~?`+HyFZZ4DI*RrDPb2l7eV#^TR^Hz-Gow9Sxz1SbF zx;R()W)?S9K^*PKJHU3=vNeXRSIsxFA97PEvIGADRcLkPZ z8d%qbI=*@1{RlgsHVSFvnf5Nv%h8O(qTk%JXUD_6B z*zHk)c-=)kXdLyIKkCtiS?Z~GwiutBn2?*U46iN5mA~|@+jfZnJvuE67>Vwo9chQN z3XxHt(NR*&8&3F*d|OqKj>HfW>C}oYb+2t}DNOT_MX=!Qq&V}NYV>MBni86kii8_} zrPxdee?#F=1MzVQRa%g{V= z@lHWjY0Xq5(zU^>mO$BvE#9LuCT^XEc{xG8PqXo+)fGOsVCXPynjw_$zZop0DlKBq zIU^M%_kO0i%qexaXaS)<|-}}Dm)ofU9se~@z zhTRp{+{Mw|_f)bEU@;M}=~Ek|dxzuCiUgKOugb_<^*Xt8MuVLUwRq241NB$@Lh$No zYw$>o2z6g4lT`FrK)F~K?Xj)%UMIuf^G08bQj5A!FlUp&u1;=BW-VQKIn`C|tLQPQ zX0+D+%s?gVdMYMDxr{k^SJ~b1P&HmgXCD^BftovCW@LF0P#A8IToY2#ko)Y{-Qe>u zIEWb~*u5d_5O?z%qF>HW&mtZsDjVaK*O^X|loUac9md=`UJ!S+R&+MZ=Is`&Zxl`U zWF90t2SCkEKvT+!P`TBuQbO*2A=M}8-w}U-F?VsrACAZw_!<>`p%`6g6=4;LI6BD}=rH)_*7n`<0f>Fch&@LK|iMpseL$ESd`)d}8 zxU0e(cLx@z1qG2(7G~d1fE}2bvEn8J9t*HY86KvarrGq(c1!X5O%)`pLBACVn8xAp z_&GeBujdqQNkbk8b9(azJR>)L>D^3A(0=ln;S9uDbsK)p`)eNKL>%8MaibZp}|D;N^unL<*bM%$2M(6zRV?>N(Awz zNJV)(5gY=H^M2q^$x7U$R!9tD{&C$_+*SeOC=D$+a6tK6lbuW3?R1s$>s#bNUgIn& z041W_l9F<6v8L{1!(*q7K+K;At3*%JGLRNACjGlhsZt-q{)R3{zr4qKgBKgPTMhqp zUcC0(i#8MtQM21lf;yz*B)zO(;pHHNs-IR_*qODT9+#bh?1%_k;h1h3mK*i*6 zaG}mg=uL88VNpTTI$w?)(Go+8_cNT73Qp|D#M4fR3eAK~J*B5Y&55jfASOneM%zq^ zx-!=-uFMLNzI7U^+)|exk8MM(r=^%-5-1zn$3Cm^oc5j>Ijtt|jby6O+$x>cCAWRu zFHIw_t$T#D8^6TLFT)b|Cj1?BywY&0AM_ETT8EY6h5Aq1Wv?yqykiBEqgRrgFMmA+}a zia?bEhu_@2>i&SvR^7j5$&~X?zyHRWkB>R!qYg8hIf`J1@6oMsfReaV6o{XS0iw1Q zRAbQ+&LK&Uk}&OINHhlFIFF24)Ngp*L$8rU@!p}*+8}i{+JH~RV_6U-&=a^#879!- zPB`t;xT7jx#z_-f8z7%SUmlLG{H?aT5NGk-zi&eG?CzTLzoVUx!2>BC{TOjcJKlZw zO%S{E6&ObrNMGhjxfojy-aO~{B*#yj$$;4756<;;d1)h;FV(^k{aMhZoadyY?wgo8Y0BGK0>=xy23nr6N?mu{V`o1 zic5u!6x2*^4>#ZWiq5O(eTAUp(3QfGXd@yxF_XC@4NPnevJs1-0e>r+ZF#pR1_+mR z(=u10$?zFCcFs)_h0nw8GBCjQ&DZn(l%u}WrHvw1b)=^K^$|1Yb%W@2AzL1j2A*__ z040;U9-Y|OAB^A1_Bg&WT!Y@pD<3QW&i%Rnytw)tfi9&^`{_K$DyVn3B}x)YtvsD! z>Dga!mCmxKoUMN8XFT;q;>sn;fVwHI9Fy>s`cG6D+HqhOQr4LJ906Z)meimt5ucL z&L3lr9K5_NvGr+m|50$S29sAynHDIIXj`4LOuxjhv4>5+*-0Zj2G4(2L(>nD z0yl)(m{FxZZq!%Xs>rk_mX3=T@J{f}4+$mK)kNd9`nf#E*)ZRv0Qa1nS5`U_6KLC!uh%tFe@yhN2pE|;Q=%r>KzZ*O6FHp|93QB$1a zxH0VAmUXn%YiatGcFC7%Z^UL5obLCix#tmhuRqpSx5qCT^(Eq9B8|!^=zlDM2{^fO zK=lIDWT;ys4B~648sX+T zd{fNFa(@3(1)Ke}(D+gWUc~ysFa^ilF-B?T3=cKu*Oz6rVSsQ5^Lt~sn`z2oTNodW z2$qk25Ax@WyIZH3rWAS`(!;;etnC#MtPN6ex|Tf!AP>?_dqZE-p5<%ZR#NFWDRJXQ-p3=#ZFuIHq{Az5AR z>;&$Pk9?7ejdkwIpj(t+$zx7JBQ-ruHC->nrS^TIPITacRC|3jxB_}{0D~M&{G<9JI`1xH^s{N9!^>u~yISo_UbbSDo0w)?oyhFskLtBpA zC{+Q%hc#O5`sGgx;4$zY0$yj}JJH=9SvLc|7{chz0e+wlb(#9(QkzboznM-XK1b4H zt9?LejaC8otUu3*K2_!RNFyd5O<(xF3cj4sgD_GqoGOiyb>G9!4{_+# z#Sp^~@yo)79nMuh$LfsWIUU6HU9punPGwjy>qv;#I-cDq>Z?3YelC69f~%#y*L}`% zz_rJJN?TdEV)5@2YmOB+zegEG#}Qbx;gSsB_3igB?Fcs|HCyvv#M4X{z9?fJAw2mlJhJKN#NlCa3g#LqyX<1CvL~|PYXqu{C`OG#EO+cJn9vY&5f6!u@|g{h|SK`4QO}z2izh+!0!At zkP%s#@o%lLcp0r>iiYBV?sGetE{dpRBdu624c~A`{XB`N!R8%$GUXyM5c@MQg4GsP z$xDRiqWQjdl;GmF`h|l0FcA|58m=@wM()6t?F& zPR-KcPs}D@(hzpKtAp~eh;Qat~pi~WN2^J~kNSgBYzMb}O7;{Uj zT-T;wR1~u;LPm{A!7;vYaDNJL{PX2?vG_xDFq%2rk+~#6k)M{g5Yq8;5SdPNu2_LPLEv=1@QB%Ev>(1a zk)MtuXk-}N`n@9gAH2uXvUz72|LAU6apb; z2u`r5X{DD_gf-TTY0lhN3`_2L*9YIKly#}F|42BpCB(e>{f1Uy19sYOXuok#poK`e zn1oVA+)9rG(_wC&=9JEPBZ=~3JoQE@m1=4R>6jA`MlT4D5p=l78Yek58I{otUM!Qd z&rWaVCztyXqTLPE@^!0w$59M5psZc;$=?ZvJhF7$;zDTY=7y6QpOcJCk09(kJOpzI zucd85Qdq0TtCUf)tZA75r0*4SVe&dL3%sH>5gsO3Z18)h;}ir|h61Ow(d8kys~>%h z`4CKw4D&F%HDru>AVRdKuB9c@5eNgII3;{UJ&t0Wn&7K;kkv);SGOk#kHkXmod5t1 zk8~r?r1+l1)XuBdnn+J;Cmzmx|GGQ^Q1?KN`TktCGjT#V${KMrs*iQXZfA)t;vp_c zn*BgP+@t_F-@I{M=-h%6ur*f)~3L8NP>2QUSXUvc7(T= zRwnc<+oUhQu)XKL3_ru4Q5!8lpZ>8Qwwu=LV&zz7T60aBY)YDQ^g+TXo2f8Z%P31* zFb0OlXpq#ZZ>OW!vT|>8x_<4le)M-;k#b0%LIB6LU*rdatfd`>cTnIwM z!X_2s;7mFX5cM6f`Oy)J`IBsTV0@oKYK`>;`Zyc+SoZ9i!}o`ojO+&6^63hZ`M21c zM=$WFt#x{2ViZZ&RSt{ZrQr)LFoxcUc(vTGrBwroKgkUsx=A5^rNC%+StlBtJeY*W z;m^yt{-3G5y0Lz;wQD7ifzJ8&mk6dVSfG#kD`B8rQPukqB5&?C{B>)^!~_nx8pJKH zgDLV|hNG1ASGJgMliufV#Em>}tKxp)ES3B77|TrK&c`E)>7VAKk1u2<$7D%3XBWiD z0qA&h7Ae4LV*oGCw|*3+_Fj!WdmYwN#2vgjKq+C53Q{vwBXplK@anJ;9MXeHyHpHV zh1dfVg8U<0S%$In%>iW&ZF4rqM>xI7-i9&D0ey-3m=h2Bg=APuMD69Uq!MCYch7r$ z0-U?VOjU&v+K9%4(BYsvsW1xDVEqi-C z{L&0zIgMXMUD>Oc1fB__a-w;jMD?%&cj!_wO!0t59>X76Lh+d2>tQDjC8FwIx`A)L zHoEvUN^GTT&r?GC2K}_{7Q_A%Mc4AfS9e>y$mBNd>KpoUOEo5O(JVG}d>Xf>4RTmt zzPBEX0qE`*TqWB-B@q0qF-beF^GdWJ#M3DlevaxHDl7`ypG#d(&|rVa2-&`XFExB0pmrE3 zUPl6-q3xPNW2a&h%f{MCK1i`whb1wIKQo?LsD6A#k1)B3RA=2INuZ+0m66-*Oa46j8z3N`3K#p2q7WNk8G{OjXt%bYc|*s^lAO&IGK>lihl zgEln+Uvy{|=>TVF-n3VX0i(oIB>;J0#55=Qx+r4<`Hqq)Cl+O#CSV80RO!MvHds3h z56bb;X;BP~97Bx#-m;9w3y1!XHg&gg(al`4O9HD74#lL2bC#3yERWsbbpW~Lu8b~C zKIo8?P*elQv9xq&%usbNu!g^_RK{D)mCrlW`WTj=@O%bjbi|(N9dmQQ+xPCbFsWD8{$Dsen8!pY#= zNvAtUl_SDo4VVHqsQb1sGVvRE)nCqPuvh)tdLjvpSgl4T8-1MVFK1)(t+nN1K9`z) zt_$m_&~F7g^6Lwv7^W)X`mgy%v7tFZRz%o*;0`fdG8jvM@Zo~AMBV3$=C5z1hBC?YVdPhK?oVR@l7Dos#?c0W^o-=bsj>%oRJw$ zKSj+Vlfu=YAdWBR>)*VCppK)_+z}=lNyOu%li{`SFOS>&2)uqtXA9cZ2kvRcs;;AW zo8QJB;cn1>I62^Ug=Oocm{vyyJj{MWc-Q>iN{!&=WPxb;dEnpytRJ%9g#j8I}+nm9cPb7Zt{QI7l3&?mKLdg*4 zNY5zCK`mcI#MGAV50d~RbZl=Js0|;YM>}5qEIArQYUt*hHcxW* zC!+>YbE%$H7AmrQa#5NyIUNuCV*B=nmX2ol(cA1zz)??^q_LN!562tdd<1HW=Pe;d zDC)L#UO7Tq)1S9-IB=xp!`4{nePv~4UnP$rUuD{_lHj@Y_uoMynPf39cQ^qC42~zr zA#3kn?3N|9Ot^HTOHy*!fG2*U*L||J-w)RUl_Q{|lsYNl;p>@`3u+@1| z>kwBrx5f(8GU)`_qgvK;V69*r4XpHo2(9CxO#X^Kyq z(I$rrbb8}T2|a0z)LY%ErviWy{28(_i#n!m7i}&7Q99mAXv(0e#PH6=Jm6euts3#QGUNKP4JL_PNIVPUbMERv>DU&N#@41Zwq)8Q;8fZ zv#f7#FhUt=Ak1soU;a>)wsv6j1r1u$;zk6whM6PAhHojN;ybdD2ww3PREutJYC}lD zk^37ia{CB{uvCQ6vdHZX^JTb+>jSL7sGWqyKL3PVY1(!T|HQBV8WWIv_!YUl#oYaf z|Ae7`n+-%B3iy6n)9%KcS{NXn;`+tQ&9wp7$eU2sYR`{zM--PsTS>iS&^LI`!)n(N zNPn@v-*2oGjoXF8#Uh@Xmra}rsGa}q<0W>LKA6umPmMCv_{;ldCTsxA7}6&u!8%^E zBvpv*52YC*qk?$xP=u1xJQ;LqIkx-=t_W2Fly0cq&31-L7+AN0e}KKnBjgDxjIwm$ zCm$cz_Ph6bmD-j3#i9bP)j0e8E32oTIg4y6UM}o|bu3^Umk&|Tuz}~O^ z08scV{W~}~uU_op%mN;qQMRU91XY!ettskHn5>$-1X9mvTe!YSgY9YY9>Rg>J!rp` zS&Ue)qH;p0V`PCH40U7PpKhDeaZ3I1G-X5EmvTfx{oZ{eCbO7IIj`+VWdmVxgkg5i zLJ^+)9pZ{Y-%cHGk&wyG*P$a=+S*rRP|O|lOb7CB1r6>=Dh#1r%c9xsN4VyxZ1XFs z7OFFX?dc0?>V;oU7sZz>F-^)2@-mq}h9bz^glDs;5Q5C_zrTcbo&-{K{yN2R1N@9aeHd&V{aZUtzkNj}e-_?you~j&9aIrN84*`3 zI%68$CaIM)%3DK|vk!Vv-Br*vlYlk}`W_H&SJHKnG>2v6k)te3L^6x7sDLOnt>fVhN;{W>c8?keie9P z5M8OnOCC0It(?`eH)j;t99d9+xiv&oGnpg)<`iFh?YMIDc;eUn*7o)sspR=k=Rrny z50KKy^r1FMGY<=!ZOde%3)z3))o_Z~0h+55{%fY4Sw1Xj6kz~C!6ypV=@kpwxN=5{ zFD-6j=FJ009RTJUL+o>yUJRmq_APpZ*t{V>$abOq3s!P^lD#`MY7+o!M?!IU@cv<0 zhkX$A7oDhtPWbYd9mDw^`<*p7u$eDfC?2#yZ#!`@Payf&j#3!-D?E`8Rf0It_p8dz{nrtl%IUKB0ejnc+S_y*+0}(3FCwZ;d*~oLFb=t9<(l(MEnP0$LhS7=`&=Ev{vlZ0oZ2gtWuHD@O!`yqvk z#$x;E`|iUzA2x}+JbsR+x%*GU&^(QOUjj5O60|N~Os>chvHs9w*@Ee3(}kq*O98;Q zbH2$kQz-uDY#P|sMK#~@VKIk@P$+eE`+Y($@Qt+L3Athw$qhOw)<_&Dhh;`IzKVee zx*hGD-Rg=kxzPO*l$9ooWN}U4;Xt;{g3&qEGoWo@6O5I2_4t+GLeDK z9=rTriRozF0ozYOJ8@?BQ0QJ>V((^C?*;l=ZlWMvBWc+F9SM@*7n0gXET+i4I&5t-{|XtZrLLJY!1pz?Akp){maH8-E^td;dw5)>zC~sL(M~?Z%#9XHqAr{B4|J zQiJ(s2QK#N>FYGoPk#pl4@-Nt6}?5#ox*{@n5byF){)e#O#diO)Txqzn`v5Ozm7OT|DU|0Suz1&c@&PhITIh%yI^3b8vm*De1y6 z2pc+^{s&1?Qc6-zm`2jl$UihA9<*LEg#4)ajpuD1R6_7}_y?o0MH0 z9BfQ&ztaQ!6CZ#<9l*)>Pd?v?ssor985v>zW#^x)oh%((?41Cd|L~{uKa%-(!avDq z19bm8!GE3p?*#t`%)hX5hPL1I|3Bzr5VtWjcLuQjr#`~M_8u_WbgYa_06JDy4geDi z^Y?|BlS%ixJ}!nfmc~MM<~F9^u`vk!Qy~`s=XYTLRW26*9TW5SNW{=V($v!2!sUBy zeB&yA^QHC;!UbTEF*W?R)@+R5ZW{k_0p|23bRo ze}^zk99-YC{;8L^rHv^IfRp+Er05^Rf72mjYG?k9!O8Nk)Bj#}|Fh&G-*A>TE~ZZ3 zuQrA*rlO|bYti%{(*J9TdjN-cL%N`4Tc7Y>{H-^eKi#j|^k9d$aR?_Z zz5Pwc*T&p}QyLo|)HB&_w;GHmpECOjK7{+8K_~>eP9W37Qk$33sqTW$negtGm&~TZ zRgG;zi{ZFrYclzIr`8aj5B+Q#Yo!fxc|vIj?=FI;5n?h}4NH&HM^VLn?gqimAdQP< zY7pKhB5Rt4hDO*XOD;R~@hPtn*Y+sx_osDfkjO>DLV_bHe~r#4c_ z;f0uNCU)cV#pzN0U3+w*py^ z>tAj|n62^Gdo$|Si7zzmz1Uw_g4GlH!1vRaxmTZD;sx7XV3?s@4R7Y+G+Y{yKcz+X zFV1_}b_{&cbx0R&!JQ1GGvKjm=;F;A9f2{Y?;}b|ltmg>?mfuY!>k23F;~{#FIeJq zo+M|;S_FePzPC%!y`?<*?d!(w2Gzce5rSofUucrw?p6~CACrGPgT8rRyce(H_5ZTU z-5-bYMhfNzqB%+!_AftW)}z45wIJ*=5s+^~eNFHG5?eTQ$qd*wP(Q-$jLD1`A|n%! zGnSXi!n`GssL|Z1w<9pD2HDp7VxuHZVY>Y(^9N=rwU6pzKKyDuwoU){Wo`$k&TH$X ztkkcWrh8dv9x-odA*~-y%;?CDmm|u z0s+(Bnmzy>#MEBL9q+!a4&a@opIrgApejFkq!{V298h{v;pm>D$>gQe%oQC7vh-%* zY+GgwM^bEBAbD{E@gEEU7K;$uwgN8>>d(S7z@&9DIpDJ|Z#_mIDUt1+U|8NSw~zs~ z_F;M(TN6wkwhUT7=(HRFIC6VavWeq`7JYh9;+e<(4eXgcl(-H)*KkzR@r21W{VZ;z zf;4iWEO?n1GU$DZ51!MF5_Q9nJ@(4)KHad|*fpt`K;f0Nu3cG*X2)scho|pJ2H&Us zhB#7=@2so?`SNEh)16M6d+?*TNhmj_W#aw`k21O{Q9j|6J0EHWI&{O%k$htoK3@V2*&9O2dunT!B61{d7- z&?mY^Z+$yQFpe^SqWWOfoC?+t&LVhcd+r*OWH? z5)?a zJ8HVsl;FE1O_=KTMou?dp)Kl|!=qEYi)g3_nFVz*8_L`wEfK-DTQ34 zd{E08mjta^XGw1}!d}&KK8Wm+CEX?y=vd8jxxGWct>)bYuW*?H4es4O$MM^1s0GO( z_&5lECUWgaQH|kfXwi(F&s={Jg1@W;Z&5acX0VUB)?J+iB#`xj>d<4w9vNnx=t;a)c@O5}ech zrvCM|E$_S@BRlx98thBi69A*3i8us02hjlk8V|Jo#DrG?X9LLzyOttYf{LpJH4@k^ z#T_b_6ppgTkdDi*Ha+2h9el;tndQeJH!i2}`t~jiCx#!vNkb-yG9{Z+Yk`N)EX{@T zBvaoOqDH1|t|lnmGl&eN8oNR83;yVV$#{xNj*eWK<6(;b$PY?SZ4}o|8jvp|6J=|Gg=HNW?#;`6|K)`&=!34FI{u%$N*$}k2Vq(FP<53QV(7JpVYchTaF(?#Jo0W9*TVyY< zUWI--51l$hNq6QtQzs`Iv76dVst@5hm4R2(ypVP-5xkNW;%; zYY@TG!V(^HWmhu@D}#{jo(%(_^e03oeE&2QrZ@w{#sjp?NDn$%t0$zXWlmwrMfpg$ zqC6`Vr?m&;!xZE~N=Sdguf~5SVgjjT9E6H8erP=DM8(U;kIE~@NP0tLRzB=g9aE(s zmG{{dUK%A44Xf20-lS3&qW16u`PYKh*YUDF-}4I@XPFsSe~ds+^=|XF&7I4h?A;X% z^$*sTKuz`DaBX@1&zo23Jg=UKAaYf`uFx7nUOvw3b6R9Qq-OBPIDLHGV{M##XIy!6 z(mCPr`ISMkSr;4*^+JVFY}c*l_uG!!jkykH@r0iQgFT7jq#n1ARX=0LkA%w`zr2#Y z1q8kv-k~|mjOc!fIdE-2vCXfE{4l=HF}T&dUL}m%;sa&(jgt$62ah@J>mUYk*p4qy z-edm?Y@Ds|vM;n=q)*lxhPxz-qdocg2oWC0;H|-LKzwzIU?7%0kN(?5qR;6Q#GiA> zc6aTN#!sgMH`e70W{z~}3N$y{*WvRqm#}AKeP>v$M#@<#BTYR}d`;j)UK~F>BxdJ>J+X$G69UyFsY5DDJ$>6@}ogYjl%A-Lvu1T{e&e^Wyd)PLtsric)6jE#NQ5W%k$MMQhlmK`^q+2s_onszqT&$rpGRI7tOQ zB*afh*wKk=PA@WW?^;zy88T?L1pypybcY%y`3Lf%hlZ|P_=c81`eYo=R?Qz; zX$Yy6&o#$eIWz`nu=LNUQ=JIimn1z1@6jESs^fwB#?<-5W7!{w5m$vFn#g$G@y35+ zMK`f6`xT|#xwYQ(H)Ypg$SYg+*f+L2Bpl|i;w@_L-6N@#m*Ac#jP}wu!A>3GWGImx zdhU98>&E+$6uS3+u+W@G*9<}uG2Eaq-v;Lmwwo(cmM=DM)h}UB-sl^UHt@i8ar0hY z>HX}5aw-fTMcD0QaCG&>zJoQzFw!=}-hgxOSqs_Z*Q(tD*MXp|A$Z;b?nXus z=BCJ;DxP|H%H=tm>vk>AJj3{PHX}n(Jj)AH(JIQ0`$D7xX$X~ziPu+cYsHL{$@zFh zd>zB~40lcv)%dLxvO4SB?QbC=EAgU7Mz zZEZC-?ve0{_r~Mz--{1i{SI?Fk3A=go5iK1%I+NS?G*QNC)lwWb}8(I1nT_v02bV( zD56%3J-nPS$rFMZ+= zTd^_1?Qrh8Msjv}*#!PqerNaNKt62At9?WFqty>OmRBOdRI2Uqx=N<{TchMIJd zXtkK0DXD-AcMu+HMqnR9jGqiPc-si%S)?35C~2oBzs#Cmn71a9Y3}hdI0GMxLZ^&@ zYomn+RADln0gp52yx|y9Vt9gjy))a!2k|Q%^Rz?ffgd|*2g+&fnf}`MK>5As%gs+c zv)xDpUn5ewRf(44;Pq#{$6;H#Ao>D+2;!TN+PcV8(C3GA=$vmhAJ9hiYCQ~(Q_um0 zFR=AcZ>rUkMP!PJ@1rIe|Ug$DRX z7zGMqPWfzA%m*x>$#6!LnLB(uVAP{@C85y&RMj_k*h0sdFl)g|I^!imrTR6At4I95aQX0q( z&L>l)hu(@M4yT<6{+hE@b2YmBP2>yQ_R8=;pH117NBr9esv9h%UQoKAtLHv;)}<}f z*)+A%`g^g+a=!I*9N+z|HyDIu4F?=-0<6~rZa_Kf85UZcA&o-u z#(#HHmd6)JqpXsj2ep8D(b%NkHRQPGLe+fG>El5->dP6#=YU3cLQ}1zpt6@l>siuRCnL$CiPd$^oV`>v~3hwVyjt0(IE zG51EdhNrV%P^;uGp9)RBGvlyv0Ux=9x%^(HzIXGBX$;$)zK$13G5h`q>DiE-BcAa+ zB1r8>&MC7p!eF?cF<~=Q7Bn;uWHN-{euG{=EJ|}|Du1(0+FWz!4XaSna4FTMBjoR# zNaP_~U!_X@YB2z#hlWe`RQ{3~=KYCIovbdgQ}$>k#sLoJ0Lx`LB!TDQ^WrwAVl$?L zvNjF$*k7Ihp#ooadb1By;qK5t z0jQ}!(9{-KKab|1=2nTpdvmSsIcOKy_Vd1*DH7qRxbg$D*4Oohck^p-gu}wp0ar_u zMQ`{W`39CYFfc2GmK8}wovu`z2xK#%A|)-|is)!*1A;FXVrbgfI*79$&8!x5+?)I? zX#|}*6R(85$*MguCvi}0SrEZU%NwCMp)G>SN41og~?fj9ogsHNU9 zMQ@r_sk-KXkw`!V4|`gAJzay2GIkLnaJJIiyUp)x+i~`;Sg_J?;k3XfkXZ;_c;;2u zlM7t@>_@I6r1_q&OiWn>3A*U)CE7+?>3k9N zFP1xi=^0M9?B4G7a=mlU^kZ4xxpCuaP?_tY^6aF7`jAgB4w{ z4r8d^GGLCtW@A3ivg2Ar0m8_1XvRI1q%sSr7MLcG<4Ik{-bO^TFew3V@KqI+YFMQt zSQnyqCj0{gv?`o<5$6u7ve^X*r$Y!fk`Li`Xm$fJbc^K;qgER;4bdr$s1goK4r!_uiZElQPS}zbVmqS4 z;x{lNpNQkTC%crE&|0c23uRKEn(=!=;2dq2)5y{}{^)3~?{dw%ad7nhG&X9~lnFwc z=ruM#mFYcR3qo@lq+NbEv9iy(MgZ%mhlmm=2rvm@#?atx$qWWoZ1IOHjNKE4Q(rzw zS1ZHWZMiUbr9b(jkZ_xAe;q|B< zvd9f=?R<`h>s$%dlQ79%AU4Euhj7VZ<~9IubOuFlVAOrR)3*e zbokOb7y^~YCnDp-KeqWZ<;dLRI#lv&GLze=9MyUNv)Vn!u2W4xbG2-2(=CK7CQbs` zz=A*ZXOD8AP>Q|*<=M~Nv^r+r;IkkO4>}jEc1!alg-jJfmqVqm!@%PK!cUBJWZXf$ z+zz>#Y}EJts*C!p&ft71>w)5H6vbAK=gW~{T=lJGd}>SDR+WPcRQ*1di&GcAb9o)t zHf$hq2&*_WHQcq`CtFl``xXhCR{TeZl^SX6n7VLd!WR%)55G6E9({o0F`LH2?!oXl zrJXN#(102v-!=ll;O1Y8V81~4R8@&Hg!*314)ju4@*--ng#6e}-Qmb~eCJ>8ZftR? z6UrA1P)|t%^Oti~sFf6cetHGdQ*_kX&cWzssTp0SO2S2=*&0g@Eyc~BMtT|46wL-+ zG}hhK4q@Bkdidin!-e#XyT0qg6r$41h5gVkfj^z7&{M`JgLUpmc4Yn+W$yqTS<|k6 zPc#$T*2K1L+qUhAZQHgzv2AN&TQl+G>pXa$_nhzjpR>-|>DqT~RaNi3dZmATUsnlu zGF~DT%iXpj*%y^5&kFf5R2Jxvl(SSuzwH>3OD74r!efyPr9dMUw^^r3y%fZc}IOO9Wd z8go&Z>eB5K5P#`8**^nSgP4xPU&V>+DHKtcq{MRD>x{%Lf-#RA@%JvT%Ow-Rm{f*L z1!ZM{?tL@M3!Da8LT59Eir)cFja;qqf*{P%@#c8gDGJf1UFO32lI#td`Jgu?`a@JS z22yh4`J(IlC!_e22CwGHJ8(((^R1L6bR}#}Z124MxpG{yFjYkOKST3AU*^^!dBu1^L2a( zhO;I!keFk!EQZ}X0q$d1bXZ}{8L^*PP}J7hhTv=%Yzae#cInmYeZ|m}r+A{5XgMjc z_l55=?GR70c`c(I-HfsJI4J;UAI;B4VFwTw^Y@8#$q%!SHk%Wml9 zFTZJnc;7CHS_mKbP0Q$VJN{O|K(vR14Evo{Sl%V**C)@sv_zG_1Z#?=lR6RQ&oKd} zk5q_>^LX^>gz#=OeBXDvuF^w&{nPqH?@woU;kM`9@Q%J$f4r$$e^kOzhq zq&T-2S}*uy3@|CAu2@UX(QsL7Uw^;`5;gq2&i#Xht!Cx>h@na-eJ!2Jnd^5IUC*v4 zw-t%8`G$S-g#;+=tP;FTZI_9I80$bTe*g5~p?5<`TzXp<;SKI_AuNv-e3UCAXo^I; z0YVO6%y@ufr~Hszk|)?!PLxAPOe87)jv>=y0+VfUdoT-;!R?#m_9HM`SL5e#UZBpe zejBW=8M^U;ynCExI;b3^hILc{m6uFdS%-KxsC(CAfJsQW2nhMEMYq9G^{JcQAGRgk z1c(=V2TPsdmKMnxM(mFFp$Mr-yD`Trj1tqMGaZa|yipn}OpSv`+J&#~vrfAl4sL^I zb^|HiC0So=ow*71U`9w7K@?vNwX_Jj4nh_)%HP7D26dz-7mJ5<1lrWHQ6___TO+8b zD8Y-Us4CQ#md07|dS80QzIPNd)<~o&%0e!>3$6gb_ z^k?c*s5wgU;FSHr)0NMCeUY9&6pre3mWiZ;Bg1Oz2NBXMi&?9nUO}dcj4(&$2QBW5 zNyU+@kJxHz7XpFk4V`Z2h6sg32G1pc)vX}k!$dbAM~dr7Cr?rwS2#3hw7lQA?{cI~ z;iV`!(FbA#a3tbeDbp}HHHE8gu9`@mf7+;W3nL66Hw@otef3JJ7;jd-tlH}GO7ppn z)DG;I`ZSp%MxTkxJ#88NP!SkmuSdUQXKFTRj)-pu^L>R&vnMOG+HuDDb2Ix6ekXfU zEgBG|-O*m_aE}RdEzo5TY#>O+LC?@lQNQ$^i6cG4kJ&k9I%V+TetPu2AARsFPQ!yv z9aNut;~|j=d-&bHIJ&{k&tzFo*e&2jkQX4w`B|Zd2lrzI`@bEM(j@aQj@)-z!H2bw zN0M3f)+O^4^sU&DCkAlleyS<2mlF@Ut4C~8nEs55N@GnRVn|mM8U|t7KS+lSevK=f zG{z>Cl8-!#YxOPmZTGa|t8MR!>R!)pxSsdHj-!$Ei$+kCIgOfc)x zchti^#L3E%STYTh^W`@x1J3Y${M$@J{&v`xZ-RYsh1lfj5LU}&t)|!o;R4r+%8I>C z3P}_Ra2^ ze&t zeN1#Qm*$wiUHakmjW!2tiG+>;jp{2lOVb?KtpoXvUSNe1_*c)kS~9!3tGAU3ai6jX zfYG!9unnFW=W+2BDJVWTjkalCT)TOyH@02Za66ir>wdNvMS4>$D^) z`n_iIhOLVBN!8c~ibN#0oUxcYj?CpGZ+4N6$6c-8Z<4s8+OkRTk-`dw-| z2vevGhC{@c;4kGAa}#s@9dCf)=RR`t2JyLT0PWnS|BR~dPmjz!>@Pcdo!^tCH1U2F ztSj8qkjuj2pTJx7MK_KVl99&rG~@@~^Mr@|WCg>KSnz0y*4^2}RYA1`$Awc*<TyYB|vrsMhWTzu9?i7|5Jvt4+^zO3NSI z6#G_F9{eckHm5nzo&rJ#DmJJuQrBb<2s^F>NsB|~O&eVuUIaCXSm+9@tPW(6W6fZ) z{;Fjy`iqZerO-?n9l0owX^AkX*BX63Q0Wm3lq~|K$2rMPYIr|M^5`maf4zrRjM1I@ zw9R>Uh&E908IIJ}mO}u?k_(IsXIV8R9d9Z27YIYkfRIHpv7&(SP+~HPgQ2s1@Z%6s z%6hdcPz9oqnHsX|Q3%Mx3$CwnO0=%@+x)|3>(Um7lZ_SudkQezi>8)FMzdZh-N_$s;9%{CTP+{arY6dmC1F zK1)|+u~zCx`A}QZ375KzncF-!;sg?dz7Zr1ddfI%O%G`{+!<}uSfjc!k%l=Rj!b%D zYG&Zn2_U&P49a0?W0@eGptVt?0wVF(LiCLK%2pEQ#?N0Bwzx|a*uR~bXPYHfaxiTF z<&LKIb$lx`Ss&JfyeIfpjO`NEgwGVFhyhx0kU^a5*PM21OE)ms=}Buj!l>ZFHbo^8 zeWJ;wWuTYylw?5Ii_*$0cNCR_I3p1P%`~KUzQv38O zj3H#XL+1foe#Ogt&r6tY`3r5}+u+Y>U`OJu;2Q(aC^)AguueS-y^ucp!|#|8wy3Zf zQlQnBR?67LpK)saRMb@{(cs-wd~?7ONqk9a5zhqV7MU`?5fU1al~;o*YzH|zj}ey+ ze73iF=Sw4HzNb9IEtLa{TS^Xuisdd8XH%rPe?^8o^ljtml3y z97WmtaVYAiFT+Z}B%@(Ce8VHJ>gM5V@R&B^-c zE-c}DV_eF(1xg2f2!oZC+aP@a!(#VuEi_x2loV0(LAvZNJ2F>zpnCeFml*b=2>z#R z(r!U7CMprN6kQUD@L6%ut^857>AL)>Sbo9hcC-kKmtFJIh%KT{fsIJ-m2WW^`$6r& zT{3#C6%i;5qzmrjB7I!b0bGx&Ca!$}S99uvl#A;qiV)X*LPOj#*x>CCUR2|aS`7te zH2S!!f*w4f=jy*b!zj-t914baY{6d+YI2>V6hMq#5K2aYY$pcNQ5)+ccob@ODVUR+*#?C%@y&v#mKT zy|SdKFk_b)5pq)F_=a{VJFE`+i4#ht?A$1)2kHRDi6%rughks<|5pv??^`O1g(c-B z*_!v0+Ie?ii1;Wo-bIHkqAY5u;7?~J)G8#?D#-Te(#;*Az>@UZ5Y&_h65|J8BEKX% zvdLA<*io&C!~GrUNtmKX^RXvHg#EeFhJWMACYt6u;IT%E`R4cBZR9Ik^q6w|c}`5#xYP*RguS z1v#2d&>KJ{#(?<&!YI}XwuPKRbs)}c2U;OkWkBH&uH5v5`h`C-;_KWGPg+}onN2CTR zWy!e3s;s;RtzxocA%p<(cYPF8MBa%OgH$)%7iUa}UHUIpKSoq;2A1WP5v-5NG=Q~1tKTgYS(#zp)L%ez z;N4RMVeZ5<*d6kdelZkRW8bL@ILado4>xfKQ@2V6}wa+w89d4Z{h5|Yeec@P;}DM<5^Si1;~^^ruy z!9i%G%Fd5liv4DZy#cW%-1VYamGU2IVd7q0o?657dlBu^MJj5a$#_J(unQWte&5Z(P-Pi`1U`^*2Sxwxbnv?Mh(e1On zX$?TkUeQgum@KPyYQrqKv%!O#|J+RgqFTMhWi1O+!zO~|R(>L9DVx(pLfZ0h@(W+=5*JVQUv5vzIIO@asN>n^Yzq)>KcYd0phNd?Isx z86_|zBKECcNbN!Mb}VUIUx=V`A*X@k2!tqCb@URRVTS46q!)#=vZYO&LXs-e0~KXb z3Y8f0g?63omd3+aF0g!^Y(buoqN3=?>;5c|)yu)v#6Uy?t!oF)7Zq=(je~8D$1%^? z#G$VT4eKah%VB}V>Tnb3V`W0))r|_j4i@Q4m1&Kdp`cMk4(5lJ4oE4$V(oUc7Z&x3cbOL^Rjv+uuUQ>et0NV6!%x$IFH954+cT%P;NUQ4nWn!HxVd`5X9}oS>@0v{V}w(i0E{T_ziI+V%r#(meM zjf{+@7aHhd5GqOH)BCJr`Q65o<=KlsP8;Gur!a@%e4@_#X%%Vv^n$LLhq_Wu)g+BT zklbeBb;)B7{Bn=r>+e!Yu~xU2-Hn7C`LCim=v(6W54r8)CggXueGP$e z_qdQQnIHr!;sfYW8?3ELWcjWNbo-`qzBtV4Gw}I2Vq}iRxscLYVQuLky!{J=B%3Kw zsN*PUqg&zJDZY5H|W>q6uaW;+g^^m#_vj+jzbzL-MwB(_w$h$FegfCdE!D@(?! z!2dCH8tvsm*AG{XD={EE!&NWcc|7`c@z%)jLc|gAO?N&CUOhqjpMvEw(FTBBy_@qri zTW;d&U*lzuP`~`Xs3Kq~r%Ai1#Yz=Olu0aPf^y)9T_N1Z3tY*PI;ad^o3*n%99N@Z zKoD69g98M#K00l%S1Pn|(JDBn$zjfmBOA0O?59;D5JW{53xzyO<^`r!_`|1&){DKM z;1jo!!1Fk8Awgz0Yycdpz1AF|rHvs7BP25Om_>A=?Wnq-VI3bbnE{m_;ZOw3E;=H) zgf4#a7aq?rZ%3hSuP>eAN>`6y=$spDsLauW7lyh*-W}Jw6IivUC|5sQAMm1qD*#;8 zR{M6gj`c_0RoAKwT^Q50h%lH$Y%&r~Nhm@sn4;Z-{x3^}a7yQ7!mcwl)9*o@I=74+Odrp5g7CNlhMWd-Vj-+g3 z)z4MnSGpaYX{g-ZB%<2G#obTC5!FHO9dZixE)^1WG1nioGb-_Vm4=7*q>P z5aUNBhD-p(Aq(?ULf@|etFj@WpmSJt@E$V*etU!$5k3g0sJlj&v=U~<1Lk?bBi&F% zHIx4-qOUHZ-Y@@a+d&BeuOYR?=3zi_D#eOu}`qKq8DxM!Z#{(2l@J z*}e37Czd}=ck+T}_94^Q^@=xsW)z^aP`vVN-fL+ahlzognwlgRs9x%1O@0>R%;?2B z{AT3SU%MDBK@=cpJfczA9*lvaY_Xq6G(?~h3V#=1{Mbbo^sEb=SB63#s8gvgQD!?1 zP#sHOdcBa)`m@m36|~a}QIh@i(s31dXgIOBE4oDT>M0d35_?KI~0n_06m0& z3--e9ls=@=QV+#L-SpAu&1RcX>P3Vg*Wh91EFh3e8?*!tlJQw9P5RFE@l* zAkFTTa8Ke&SOB*+Ob?Z1g(clBcnzF&9Ef4}Y02O_<3;^Jfhag2t??~YkdXP|z^JmL zJ)an!Mm%`&AnP>|FDR|VPPsd2$&#|By!M86tfcnb+Ab|AkCz(*E%%p59_O8r{@=4) zDacpS+h8xE5W7fkgoz4CQePK~>%!&Ot9Vqtj*d=DA6rmUvNo}}m|H@V!Y_srUA-zD z`B?pKsHn7N<<34Iz0P#G>14YI5({t$_CJuQt+l2|`pu^Nli=wxFAoLrhDkyz_7l z{|TZ-Y@q{PdE{RA+?)g2xs=a$7&78<(d?FPDISw5c7)2U83`_b=R(T;bKjhh&z))$3I!HLnel`PS~_bR zE;SChHxT;WJDu*IP>!y3S2G2g$!}8*vc?>O#AnNhx5+nPP+s!Zz>uLWjhRPyI|EY~ zIE=a)%EoNXtC$`gp-tfG6$RdL zH^0Uzr0b-r>HL`S(q1-1Flx9nA$(^db~2o~d<#ljC12y_t*^G7kmYS7ppl423FZrM z>wd*YN$Dkv{~q8`n;z(AZs)ZDJ@u>cp2?PCK{M%SjQVCWOyvcBjs&TyWUA#L`Ly-t z^PJ(=PqeL?pO)x8DGN#o1W7vwGLiHe*xb?^v7@3k9bwM6(D*kWR90RyZMn6SI(wgO z{9=M~-n|{!OwbUVXEq>%%l^tpkle;!dmYC+eqH6!WjXp~QVh{Aa`?dHd>L4qnJRWn~QVc&|sk zC~>2^1HEojrEI}bjZyK+WqBV!K&?52AoGVTB`>9`c=6tlx|H@hT=U{L#ED)NZWvO;Tg`1 zy&7&vj809bE+3t<7R&e7Yx=)y1@5P5Th0?cwjg)@Fxc@@+dJMrMXh z*t|&7PWDdo;Jf-PVBK9Vr|7P;iKJg;;3dojvMC8Qp^RHApdMnA;=*NAB*$}#Ck9FO zkfT3ue%Xaduq&#DCm*u6D`#!gU}y+eB_xC5+X^G2t>HY-N>xF+WN{o0PQyBTT66OP zUjZ`V0Uejljh9myn^m2>*8*8u7NzJ(mO>VVVTqNs3lW{m*`Mzv^ zeA}0&IL$*XJoR*CV@C$2UXy(9bp@i;i@Mew=D;~<1(Q`mMa$t_0u)ls*vX?KxpFB7 z`Ev${mNf-&gOT9C`S_u9D*CtrCT@nMQlEvnnSF(ON2icXrvyQzln$NuRIU!9k3drc zi>RkV;jcoOuAQdLK)gX^#gzjIJ>kBMpl%67P=P|abAN7o243d^{`lW8X=jmL0VUT- zkf^!8fT2j{cb`ccPG+-+)t0q#rG(#u|YX}gI1H+-zYWjJLmA0 z+S0l)7?pW`y+19PO$Qc*45fX4Us|KrKe@9%3v1o8RB2 zmJ57;950uuw!gn^q80E&defUsguGu(y@Ql2#QA@S>i(;;@el3Thn|d{k&TY&Z_yn< zqV|u<>_00T9h{B--0_d1P(fBwN=1}X(AL?(+}MFyz}Cu0*1*c#-uW+KZ@f35xZz5o5lzt8&fO4|P}Nc`yL-)g&m-iLn%0ua>!w0eJv5C6yE{~lc6r-wa<><*N{`#EFheTby4 zv+b$Dc!79z_q)A}gF;Q1Y=UtK+u0}rn|Az?bvlbo)_p6J^OB2Xhnr@-adSI!7WJ?8 z2X4PgCsR_MqrD$5oiExyUS7N(Ub9jHJEu$56G1HpzJ-!O8uZ*g?b7^*IDhdc9Zgywb{kkfDK~m!krGk z;Dnn9_487Gz_W8IZ9@l`4~v`G6fig{k>-E`^LzU*ZL|1 zV_R$+ZTYn%ko&PUh7>KoK9%?bH~&k4Qb6S7n-R;*i8i5HaRG!Fu;jv+=cq zdOD@M*i7kF?8!jIcgI-0v6OaO4CdlZVpC}qZV~u8yJw>Qhm3#X6} z!O{2-a}9S(Y2m*Uezmf~=cn*Y22HLd4=sw;bLEdfcLg2+Nb>IlafdDx3js?)BVLY zX_WRuhsC(;mABb(X_!f%65fT^sG2OUf-!n4Tucae7HjukuafNSXX_cuN-B(2?a93H zXZwRII@C|!^fW=fFnBg0b6aC}sg&0i?`ZO&faME7N%4swAOTBlvmzh`>N|^bd>u@9@Db{5${Cu2ZmNO)fC>e!TH~+* zYs{UW9RquHmjDB*q=C4XqeMG|MmAmZrFAi1`J0|-bO-#$mJ?C{}E$i%X(|4^Z z>2PqcL|VWjEkj(Y39k*$Y05}pY|GX4(6dMS>x$jJwT2G)_#ki+pAdes?(C zf9HA5!Gj_OWEuE3=k|GxLgWFtjwfb5Y`I+_xkOpMl2GCohH~V53jA^jf^y)G^*@C{ zD2Ebv`lJCT$2cr(;@*y+RSsG?M;>(-KmH zGPkKXw+Z-T{ZC3UC#O9Q`#o6b zoBCMEgrIo-+Ud$@DI(eHE*r_pyKKgi_q}fl! z`@_iwJWa)nZA`D9F;AzCs~(OS*Vx&)Fz(FF1hwBrfAXeHOlq9f$!2`{67{MIJURJu zG`5>vOBc`Pm!s_xYsj1QQI4f~#jm5ES9)DM$s5#H1L|Y^ue>>R{fm!RC7L4QEr{<$ zzD0#MImu?sR!83FuMwiV$n;9+Q|qLl$2y`ALvIhW&8QD1=LR;x-H^iUaV_(0dNyU4vH5Q2ZmLCzH@I;wD1E z6)dhtg$PUE{|T4PQ>fVVBn|g6)Y9HsZ4)xPFwQw;i{zu`E}-tFUD!Vsx3JDb>dl`Yo{C8654 z@uaDSzSo(JqU>;ZR|1u+1Y+TOo55SU1DQ65d2%+P(i+FJMv3#6gBO#ls8UjQ2S$@2 zAnhJu1W!moFK{-(Gvec7Dcr31x>@mYu>fND7?{xE&jW@aT3{}O14Vt=AX3gcTEsw9 z13hD*jSNvJke}(rB=uueZ1X*uVrRnH<;a;VFfrubQS$Ds6OAaUx!6 zfL%n>7m%LyU?kdBpi~-ib?+u-SYcb=36S%F=#Y2M8P_jyB!7Dpz3Z0}XxUO7U+6q^ zvi6+4$f=^oJvi8ryi){KCLXG|Q-2juftt5^7zD{qz36f?clm~K7;izR0die`BBVdt zdvX$#-xqLJy>;yoFRRt<5>U*sS|f{-?*4p|CRpN~{CYn(osbhQ9(@b$iv8rV@Y}{s zmM|zSzAsY*3rl%r4q06(PS_k^O>dT503`l}9t4`3DWVWv@mQVPHef0(KMsk>q1bF6 zG%7RqBlH)MnHdQSV#A2K&y`1+Onf&DE_9x~5LftRaK2ZXf>Z1|u+ggDhx5~70VgYk zD#-srcIyW_NB+E(_1!tsa&~MIiS8)06KV1AF83rij_W+za2(^fYGC&P+9;+Bh^K^H zr)nXETiK>oCwve&mHrJ=Rbt9LW}$$QB0+r_{Hju@6@?HB3L#b%nSJY2?hONA_F?_c z_%yr=GH{&S=l8Yq{eK4JAGL{UP?t;O5gXrg8Nc(-AM3x0g>{?H3{7{%G@3dqc|B8m{Wy>c@LLO~S421t&PN-gy-s8~Z1Dx@t<#9HSx z3u=y70XqU!Z4SlV--L?Zqqd%3Z~Ee8yVifp6_$3jjuqD%eW9%AVqW5`ob%?C^K_pw zkR+kK=T6Sh-M0Q#O0=LY9Y3U9qb7pQZ(t@2ori@%a#A)J3 zn9lCEJN*EFf0);15DH%UX!W884)H$arw z)i29xhK7+h4*>^02&mW0?^?+&3Y#4BQ!RcJ(1dZT#)|?d$rD`q`Wtb2x40>_tlSt5b6g9%rLcxIYB>~f^z}I~CG}d4cTQs9IrsqT6fxanm;%6oFK5u7R zb38}Hi1LCWS6u$JKXdZ7s67G#4whA;Ce>#aSJU#I2fTWQkL~bUH9GX1?E76$(DmWL zd*EmISmD+6ivF*iM51mXTs6223VYzf)>LW!5t)Ed8I)g4gA^Hv1F&{x8a=GsRYQ>QY&o+or&wdxlHel?XK| z@$X2Mv7Ol+8Q=UaLs0U4aH%^bzoy_wkfW2DO)Tr#Uf9JnvXbZ5n6Qy2S5ZKom_W_5 z$xEXw<`Y6?EE+UKr1D0X?W_y$E7S#9!fhVbwYWT2C3RNCYCtG_VS9zwU|!f;<;$ZF zQsE_<>C?7>uMwa;@RLl-vum{hk#;G!oyyKhnAdQH_-Z3lIhdsNyQ>VtSt9D{9rbmRsVwvBeY4*s}OA0f#i>@~3t9Gs_ob}&4wxX?HxNzD_hFGGdE>>9|-USZTwR#LQxbz%>Ar4zDNge(}%@ zwshi69yh#GiZ$R(yYa@&mbA>=Z)Hpz%NpOi0{%?zAB^uGOz&6UwF|3kIriRqBY#6* zyweVT>$mz3kmuuj{Kt2DMs`-ZzZuWpmRA2y<0z&7fOwSE)#PN=D3#2ujUB1~;5C0j zg@1?$B-;l&0=Pc?19JW+w8H`*7ym*#tN_mQFS5hRzySF{YCaZb=8qBlv#_zT794D0+NmHo$xKzp|Q6HYg`#<>UtyWuJsoS+Th+Ck|z6fQ;{? z5CkGeEI}#$Qc7$96nqUP{6`?4{}6oWBL-jK4w{~^zR+j;*T`-RlVpiI_9$HyePOJ^PrFPEKHU7A02e$Iq*df=li0YN#q z`WUdArM&5aOP%U46PuVDLVQwfGkTi)O_hZOJY%iyvHnA{-bsgNRQ2H(F{-I~tCXDt zEqy)H5+c>v1zsDa}%wJk>#J!Hs48Y%&0l8e*%faMC zXC8XjQ#*}ev}lAh;UhTzIwW@H99tU@NPi7puu0)U3*uBU1O9;I&AW4kj>AucuII}J zGJt_U^~s*CHKB=KN!16vb&Yn=H|MOeS0 zS-)CZzizU48)Es$Vs#frJv81#NIcdfZYuYc*j>n<(VWRprh;>KBy2S$xS_#GLqkT9 zzmCUY<`L2fi|s5KsrvQ=8!ZFn)qT1G;SFIbaNaHf1#iXd!|cGSV0J@OvApF>;OB+W zOsjk+_vcB`FB;9G#fvv&It!)2XcqB8%r+Cqx+-#A{1A^Vyo;FLN@dzYBsnH z3U1BvK+KZauU}5)+d#bZF_4<06JzDbo3Y3$3#GUYftsVc^?dC!(&t|F%B7G}D2JbO zkW9X)g5#*mGWMqrE^RlgHU1{Ni?Ye1KSniK=q8tfo0p0g$l~B4Uogp%EuSl9t^3W9 z=U+UGgF@mYja*HTLZ%9NmyBscKfx%)EXAUPH3p+2Sk`Mj!g`2dEfPV$Y{r5a5E(B*(nX?A#=eF(=t2ncMo00O=o6Q+NhAaTDl$GrwlU{+~M&}Tk?^! zt#v3egtF_*f%4nhkw}tmtKk?i%4n06ft{Gf!DI zRL5q@WoHq^NBKfpVgIf`!iv6_(7X`{1v6Ef8NjcyYCDEAMZop_Qb64nArda(#m&qW zsKk_os49NtE~x(L8nNsZWu0Z^QC2sieMH`;V#2CmnoLR5Z4}Buh}lsc>A}IG{tI!* z>b(}!mlh5A7!H=f78YVffH}IIj1*wVBNwFR26}4-{VLsLRJcTuXdgASl2CZlQFtYG zuOx2XgO}&;-$ZaUu4lYnh`1GqX@otFqbb)_esUx^M+n*PcZDeVmDV2I2> z6X(`H;321qrkWY@lI+k$p^fb$s^y(S!C9q!2uy(52SW%$y`L$@35cukwC>!sv>SGP zT+746Qu4B%8}b@g6+*F9@Df(iMOa)WaZfmu%3~b<<-fY& zHQ=5N*j^)ek8;kBY$oX7?YNZ~rL4-_uuPpd=Y zU1>j%SpR8rJePjx(Dx2miH)I~ah_qpb903?B;;0rzA7A_|v*!5*Eohzk@6gGH zUl|iup?~h%(sDsv8R{3~Lgu0hzy}x2K)u;y-fN3uJ8bYa%?hxlIZyjGSqJphoW>i3$gD4mlN@x&4fks#(9qQ5 z=-S!&C(a4nec$!z&XS5{YZ=}oo(%PLcl1^r0c}{rPDt6WlzPO!mQ! zNz?|LrC}K(wEAx=qARAYKiUS{_R89Y%wSv)ynyh!`P*gb(=V#b9W z)mB_Zmtew))n*2k(!;G$9kQsA?*`k8U?ECgezfh*CS3Q1OCvX_UCfo*$i+sEw5S$B zP+7v3K_AI@vNG?d-N?ggfQ-}%y}YgQif~!-i;hPGAZx!s=LxahHunr!Q|A)BfD#!- zM<*qsKZu-YRBVsa@INu}NaHEaS>?YVKg4%T6|d`AYM{3APw&Gu-7?wI=hb`9pgat2 z#dGkP&f1-_GyTr%m~&>;X7PjT2aHEamrS?Bd*JWb9hLqXXj6P~_TQZ8^Q<*Ctr@HR zo)DI4cr%W_FfZI|7^@l)EwABu_|rTPf6d>?OPbznDHxB@#iggMd5rCVTHwdfJ|v4T zy9wnjXzx&^;gp!>@i^F@l~-wBwpj_lao*! zF?%KlUoF^W+r@-@4n;7vRa2s)5lKu|?(C0qYh~~qHX6(R&cHXwzd9lhLphVFCb$m7 zRDp?5qy{OPNEl`pU$-e{P&dcUi3!opm91vX*L!GV9tW(DU{HmQRUKWh-0Uio=&Abo z+00Z`PxI2i{H8e(I%1b1xs|-UHY~MoKpF2Bk1#AK`j8ffBl{WWH&u zaNOQH*$D-PYT1BG{@S9piayIwKBP_I_5yz6N<+hljn(EOgFL;sw8GVOJ=+2Hp-{r0 zLtS$K*$jp>>ZmZLJONJWHpW>UmK4(SVz;$FK4CJ8)k`a<-gCgapSv+eK1QVKwl{8G?z5lf#>-w)~npJ}<4OIak=Xi&PN!>e9hjj$LvZ zJ}4G($c9SZ%j`8(ms&w~I0iusXHqqT;e?YG3IkWcawPIK#=+BVg}IUkNu49;+B%qP zNX)N1KUo5D*?CS4IDWW@fjDMAuhu}{L0S6X=i=>L%C+wFbs?qw{0L^aOMLRq+Is=C z>~9{7?AuNTuAdTxs>W`ZgIvm!c0b;AgKOSIf}LypLoO9;zOq5vaFCbj(%r)ds?xm4 z%Zt<^Jkm#x882Lp4MyuL(!t>wpN1Kj)qws8aOyHN-?HMF*p|x5=DFKP+=gaUr#ov+ zqZ&ufxDQSP7Z#Z}%}!>Y3J*V{4K#@gu5R(dO*h7Z;TJSxGHU1hF>s36KFpJC6Jw)% zI+xP+i#DckWlQTHxfPo=VtKhUE9?rT zF1(m1XwZ|-Q=euRF67k&hBbve8vRbPYRFKZzu1RaB!Xo487doeKqVb^Jas#LJ7_n5 zR{lsdF-9)*(tw%0fZ$se_q2S7MT)9w%c_EMo~9wE_)HlAip%cYbXgxh4tl_81hlf0 zFw44=rA#2py1-}6P~RYj$N*MD*xh{(4*ta%@iF;r0#65PcX)18su5RY4#?Oa&cElK zTt!8(;U4L<84H~6Bc7U`-0wtqPmoEFTXx-s`WB+IG2a@N8~5SPpCnyjrJ28Viij5d zY=a#Pr~X$bT7)SqI{Gb??z%}RvQ3e9%t?ucRt z-)J;!QIVYB*V^q(0{31aMIzVoG1PgsxMK0r3iDjHEc-3nW%g#Hza`583N((G&#r5( zt)Qj8b5nt{*Yl5&nd5PtE`6eJraldITwW%k-@=u%b575z<9Q)Lgt?X&FeCW^E)rHHN$x8c3O=<%_TJwCPD z)5=~Y*GwgC_dS>?1uM&gF0R%lUyyNhrRjr?MlW^8>k=b#wwA=^N;~;H2)E%?Q3cSO zz97m`31g`T!G2Wd@_k@$hgxF;k;d%e@>WB=!xSIDmG~VfJ-DmrKLDMNFZ&-rhl!4k z^>3g<|G`K8&fxebSp|R?{$)r0ABc{+kchg5IHi(u%!M! z5S=`b^VmRR=cK(h()*r|H`wIYq z{@DWgZ}Gp*{@C^>CSnHknwfNO^W9aZ1{%~{$906#bm5v>f zh4BNaFf;#A0!+7=0Jh`Ie^`&x0a9f?Jh%Vu+n+uBs|~>Q0FLnZOo05Gk2NzBGbAJH z$5j~sF$+G!A4xTU)&Tq27y;V>+n5;v#{rH7Tn7NLm;i}CjDWPDk3CF)9e)zc3vG z6B8ZV|IX1q6W2(ScJzLqQ2h@G0== z%ZOl^#Zb;GP&By`M@=CsZ?jTiOlSFUe0y34a;8&)8SsZFoX#+IqCPX>`U-%_?Zcx-IXNDK`o zn=IF=TUsV*h8m@3u$PzyIBaIclb}Rd2=XdDGsj=58V{BoCIxdqs&Eg+@U)EHbp`@&nAa`Bb7b^_) z$Cq^}A5=fHb$97c-#TkK(rUf;w--I!EH3+nEn9DhOj!@^t3H<)FU2HGCLklbTixWA zuK~hUv0lot_HCnC+mkeb(a56D);1U6iPWA&MN8!ymL{zA&S+JUZki^_OC8C| zz@18ql-*6~N~CTn^OQ>+>B_DhktWGU)_KaEP4UXD9Y{^Mo9;-}K~MAr%2GFw>ISdV zMas(79M$yh2XoYmY^h4~8yrm$PqRhJ!q!;T{T;f6fBVm+D)n!uH6c9B7AAJJgsAlF zSgO0anaq#RstZy!v8tGnsjx>*SqpSRSZjT~ucIgK+EvV*Oyg#&g`(_mTbDOD-6}b4 ziC=j7TDLxqx@UJk8om5nYFmoG#-C3&Q&Ll`R3mAk-cm)pWx#eBU&h;KXTR3B^rn|v zro0??9v+fup5#{e^lGKMZDwo?ud8WVYuZ&bm@;eE2S#ly)*DQt23VawwK7+&(X1cZ zrdLkdGqvjS|1IBH2~T?NI3nvQjcXubq(Oj{sGMZJPeQ%;aJ_62g-Re4ddzd1+cKkP zAWsLF7GRolW`vd$P?&?p6l%0GQ18a1N4O1`9+a(0f6f?@?$~EWtm>E^w8}EwVTYx^ zHd~PTn*52-J2yQDHzn0onjWf*Om`RgS7XoQ@FzW@sFiY7C&gZMc)CKn+phxd@@hP1ckSVCUCBlcG+?bR8;P~LoHe%Ec((8XjpaESdqsJwQgtSG#Ojpw znz`QX{g)T+U2VIenTE#lggb)P+?r7{v&uEj=*p)(DE27h>d?y8Z@-Jx%?qhi;PBmd zZB|PU26J9E|K=p){J^<7nyK~e?~Sk}MX9=*)97L}`jTWYUtC6K-jy&yWFYVx*HrBG zg?uxF@1r#icVrjrlH&QFDbehOz6H$hQLEq9%eG*(zJA$%AC&)424lJ_hP>v9F6w$F3Tqvz z^fcYt>M5@%cAPTrSv=0l$=W%`Vam92?f`TEo=`|uFONjj?4iR8Dk!)hn0TBw@kn|t z*CTV16Ae>Db% zVLXxWOfK+v_5g39T^ERT`IQj#Siza1;fRMGNPaP=);?-nVp%`_b0(YHjjpq-cTZty zZ7mUNn$Ldc^I-n8m<`S6L6^0Y0BuwmuSk6aPG8mMz^x9AMvqKQ06+0q@IMZDBn2i} zCO0j*t6Y>Ce#t>;OG0Zy_i3hznl8^c7&&}4pm7gL3J!LSnSzVH%FLKoZVo)(b|ULlgj6Wn0=HMhQW^* zy72$3I8D}Jj$$CcXulgVwrZe&#%efWT{uFlIsROk zky{bGm|T~AI3i`z;POL$BAvXVMcZX=3=7Dju2b}|FWBY+6SQ6|!5n{$mZ8Z^eS))X zBgK3nW4o%V-fk*^83>!U0f&2p>$y$2%)vxpetOl~cte@HE;*V)%}jj2rlJSQt7=F8 z(<~;fH}?lGzN@ha0|yM6(Z(#%E~|C%0Q;2Aa_hp3y2nAlsCLiLbF!(VgyA*|qiIdD zimcB2393{3gLaczwh?{t>W4aC#52}25M^4BEBYCtNxG?X9<4|Vd8Kh-bf@5%LFLLc zuA*1pX<12qR+!2l&XQ_fn<3;%g~#8oZ$Ojv-A;6KCP#5+q<*$8?;HE1-$hrzN}jBs zHpqo4w~ng5MkRAS<>1C&<&n;0y8v=kYXEOK*L1t7dS&9UBL(NV%#ov05p((^59oSa zJ5`-+z^g*vde5+7RRwkUMS49S{lwZ-3W6`h1+HWH5eB}M z(nx|;@cCqt#0Wwiy=0$=H8I9;*qRx8kT!^qOZGV~>{y_n7)2{d9h1r|X&vPmLbadP zOs!}(D?=BU+tondMzzk&nyoTnfL2fR{3~}FNCY$Fa)5LvbQfz#nOmW94sxZ-BL`d~ zXQ*{WVb{b(;IPt(+9J6IUTd7ZwYx>VWZC8TndA|U1$O7}hCU4DnnurLI zrxRy-1j~dQYd?4D!jYX{P<3CGL0v4K9EFu1UZtMjJEeD}_Gxo1+iK5MSZTCz?$|Ag z?rH9_R_|Hql&M5-rYahL{5nK`W4Y{nHe)_^25n(|uIv}mYiKPp%nv~mr=mRf`Vsfk zEASIIe*!lBUHBs-6ApOwZpZbdIu2<3)+kofT^bW--Pw5c7VwewkT98oU&6^&RC4FD#CrR|c1xy6 zRc0gZa{tk9lLlt?(hrpHGhnzK$9VQ%)~>d%7aYf}CvrN9+^V;RP#dJm>Onm>@tGe~ zs3&tLCx4GbK#$@H34;-tQaVCXD4avA?<(L%nx>dSDsUHZfuE(&PC(aIINYB?)-X8o zXcH?7g?R)BfY||v)mdNYY-E9Lz`uRiU%sEEtNi@T<5I6f5+uk(mr|bY|7Jkxjt|*XJ0vUkeC)nrhY_s5-Z!Iq z>mMJtqn@Xv;kZ*ax-CS$XZ8_E;B5huSmyb5`uo4i-M#I)SvJ5 zt9>Ug`10~S?eyV8uHfsv<=x(uZ_(}9=y{%2R*kcm7OvY;Rw)}L9N9Qj{fb=S2K|_8X3_{}HoBSX#&_D(tJNP%a&bqGy=%Hw;&=j$*2nFoYAQr|CH zhzNPsMzZMDy1%@wJg+6UYGZh6&iIiQhsV=U(^N71et+|KJ?H3DxcU{(oq0DBjp9l& ztwBct6>NxtqRYnHjAd)Ynp-15ZbHu4VWtW6K z;?lANqpblkeygz^%853aBXD}3k}U+F8{r)H^}xQa_+2;ZpY@-U_&lIjvFI#)I!tT zCwlq`|Dci=bv>f&CaST#rh9%tmjdlYTqeQ_+v+yP#{CP*@TM^iPV{b^10`S&4u<@b zK#O`tHk^Ot#Uxhxqcq)Pxe^pKkMGXmmzbwuUS9{Trj?WN$ zIo__nTPYZu==}4&fMb(7e2w&s-5=5wj^$&;@sskqdtse_TYlvkL9HE**c;Kpzc!^v zF%`06`08HQ-m$%Je(8M2e2;#H@`;XqICQaO;Vq{T-E+n(b?G#*;&Vydn_UA`pfG`DJz`-$he$^)1 zTt%=cccLe)%53hTmZH^Ls>?*rLuC(P56dk=;*FcobvM+f*t%YI_}NJ)@i}1JVYygu zm^heZhwT;VcQ*KpmuhVAR9=TTv>;>ThJJJ` zH0`rH3gh4RC_+Whq<&|x;bD4Jao0ZT1V(UjDq6fmny+uL@4`7h`J4O!ej;L|_@i^V zBh*w8$pa8TDdq4c*v+iw%SL8-H1W0z_RXF~Il?t^Z7eF?jW~~j>&YybUG1hTv7wQ? zXCMs*cR3E&>AMD6tLvtqf_OoQ^Y1+nje`Xqcj-e&Zp{*!^904|9}P{^eY0^flCb|O9(*^_#S6Ly;rhur+OPB zZ|9pgHgXoVdTG)+eqURScD?DGWYQt22k~3FMLni%`L6*pA6)~|<3p0+hIryZx;J z?4)c!`pX1^tK&&XM;O3yq1N!v<|Y`W0Jeod7Z5Rim7eJ=kh7cVh?Y`CKq|5ah8+DB zbaW=nmi;LqferJ^Rw4RgL-V)dZ@ya#kGHIC`u( z?mUB_!k8gf5tS(c8X{3zJF(?+_;A4GuxDSPl!+*wED8*kFq?2rUVqfkx?E8Neme0l zaQk{Hb`M7Hf^o$fW+4zq#Olwxz^Im{d`Ue9dI!ZGnNH08_#6Q|M4Nk)X*PVX@6z5L zBVM-IPg<2X47+W2pTPi|8*AQNQ_>2|4GFDG zZe8Ok54GXwuc4Ur1!u4G53##&>TJp<&PbogKsYuDpsjjPXcIzhM^#5zNAbcm5BH+W z!poveX9ywWGCGPr-NgG3$5tSaC6m$bA$LZ&+D8Z^gW(wjVdAgB_uwX-1Y+sqxOi0J zeYW3T zSsTBN>S?_1V&cNP0fl+s*>UM?3WA1n2dm~4C!~i_uer9r^!>Vg+ATiwYR#O<;NcNXYm1bDj`t$^IuWvO!M+&cdjUL6vp21HwTNb8Aah zXHn)@*I>))FHXNqr14jRuqM-hP@5B^Gz7B=@C`i~)4USjoBruC2;o-@AzsDxLhN3K ztnZtysUQnos^?&0B!4oORaVT5t67@Tp^3cx@|=cEtn3`*>2lzM=!+=Z$xi;?t@3)z z&;!^oLL$>L@#{Fn(lVl5a+csbWXLfVPX(&wz?`{C ziHcSB2CGe+tD+ME7S?63TQzw0aPY}dES5n+9lB$cpm{}rCuj_Je>uv5zXZKU zLvGo4=g^TP+~*Um`{juyQNzh4UL$xqus$!fc~oD!uOsi;lnYPrTdFS=yaC-Sm36Tg zXUpQiLq=&H)=wlvL1Er_5^87y#gUa`MNI3*chkELL$5Ll+~qsMw~%gzrwW&2jc_$~ zTgoE@uB z_c8Z{GbdApabH3mnPtrWkh*9v#&rCrB|jLT(&N`Eo}Y;-$!s+vw}M=L=~SWTJLIee zZH9ha^6(ljF&q!TPQo>X$Jx;{4Njf6qrxn>KVn%ST4YCZ!8MX>Rk=NM-5TptXjhKrFi zcWhm>*qq{0C@Kv5+P8lew|e1g+Pm+-!U_L4M04C*2Yu-RkNSo;Vv@LM|Eq)JH`2fq za)7+Fje!M**p_BlH6 z`3e8l?*6SS^d%96#Mc*=K_Uslqnw4)8i{X&o1U+;ZuQaT5n4~BbKV3hW5hNH9dio` z`IH}VBQ-5KoOXPgQT2CgVPawNl`+)#BrB|Mv&X!>YDqfMrL$#t|2O)d`$9rpVZ`8) zAnZiq4@ka7QbdJYd1+CMWJtMEvAPgj#qo&(fEo-7E#ubKS>H%B&ReEykJ zNl&8N@jJQ1r)71#x6gTeISPl1a=#2o@HXJ2lmtYX9hUp>N%^qAal zI)xXABUQb{VDFjM;->b#?s#Vn z|Lj7A5|Ty(-i7W-G(-a-^f;7b3wVA2i1WqK=Kk3HS9%rajpI%pgXVwtaOSvi)Pe!q zmY6MU#1djd5+p{C07V798SJpz?@J&i_BlbB=;8ORY3or?__fv? z;_cI+2eH{{lm4h%p3N4@+c`m({OiaakGeQqG`)qq?go zS8k@@m8Z1C~#7%~H{i zpQ;=V4o%<4#yT~)e6f%jWA2Z2?kGWD(XU+5)O&99nrwVFA?+e=^2ZBQ)0(6ubkQ!%>&ozbEx5%Ykp(&n?XCB+K zx=xxO(@eF6TlzASo%A;8kjuoGiS@5b;qG}1_1&EJ%pdm&4vv|Hz%ck~A_`!jL&(~% zn3L_FzD^ZC$cmmX*G7Y)JeYD@!sC>E?R_im58mE3{%vAV8})CC=tz(OGM<5bZ`sm_ z{t6{ul>BuR3=naZ{yR{{%;1(1nJ(gOIWhFBX%(L&3G~$+9Tx%cOXZvi1I_W{(An{~ zba43BX*E3rn+)GwLBwAdWeR0-O5+DOK&Bm_=uI>sI?m=_$(uT*lsoyF#0}|GJ}zVd zioYzW&PISL2(pU(jIgbO3kL>DTN6#G6z>i{;ODcDIedyBGYVSW`N!V9ugobfr{W>K zj0js;*NHw^PjKx?D`DVrDDhq{>C-Qt4qiRIn%$xc$!PRD$2Gf{_$%~Z{@L$%I%bZM z3`WmQAnKjYGzIYJ+z@px55-#FzOY4NnOnwS((u-4EBSTs(!5(j+DHjr!f;6ezu-ag zW^nAUGO5115L7mEyKAp_ZWOfI)7E~&d zEU^g32YBmC2=%FCAJ!^U!NEw%gAdF+fxcq&kP%G=<1DzLWX120`#&s%LGq7;ZKWih zgmZ^pgZH@xf@HObE=-xPvmm;wVA|hyX;Z`AiKJID+KnrJrgsct%L_7btTIAJ;W>sd znsNRqUq2zCW{en1gf3F@7b9Am_29N}LY71h6$_#M_ye0v>aI&j?l1tA$5@4}t-0lj zqV(o*F16U}xi*Zv80;tZ1Gw|{#gfvQbG^={?acl|pW;o&z)fJ|~qG_+k%%j-)euBQ1ZT8l*{4L-7Pc-Q|Dm;D4shAlgii>HFkm%$m z)P8@$Pg!_TG9|~Su{C0Ob1oN;@sBAa91giu{MyLY5w3%Ca&$pXkl*!h=| z_uN;LE#Gvw?n+7;8ozPp$*HBodKB)#n34_463mrD`4T;Tnx@)iuGD+XB(d>rcv!yp z>Ai;(l$vR8GE}(wtQP7?`n-?k%H3dVv>3cqaam28Uf%e=S98q5Wj6Ubyd_dikg4^4 zt}&72%?Iwyhvm=1w)A7^LY=A~j|^(#%CwZd)7MfDQGpL6JN}3^hbYqS7V)iv6BGmzLZV>7-?3URmh&7EN!d-0x}!Cq_@ZHxeGSF}Z8W z!7!ev%1I6`+^Ht`ZT^7qHjuL+@)S0OEd}}n-M~?dG^WNWXY9*cw&@NM&{)s3;5HOb zqmI6tkd!&k=txVu7l*KywH*f z2(oPh&x^21mNBNRU{(C!wojmj7(daAuo7nE!ULDww@&cewi9vRi*V=koDwv+W3z-< zCr@i{Xp?CLcro>8|3CnE8%!5sqUD)?yK8kZ!3S-t9U@!D-NfI>>`LHIU3|jks-jiK zvJ7n)X4hY@t^7fntaVzk5k^g5hGi;}i;g7f^p!|O%isG^EYk2vF2p&|(3-dgBSPF} zB5Gjh+CGv_>f<0o8pwPmX{8usc~30^SyeU%)=>k1kdWk^TK?+fZmDg1jX* ze@b}Wc#a^0w4?cOv`FbJ&QkKLY#|Tl;KrDQ|v85NN-v|W!-R9Lf z|M7=)_$)&*IJxlKct_qsh5%=Jj7r{G<7eDIKKRQj*Ymx>C}od(MXSkmQ7+5p@e{;_ z9i=YwLH;%!pYvh0*0c5JOm1V_#{JRXa=R>P8+ZH51&dF)eKF0}Z3L|I&{2ArrgO8w zw#7)+wzlx;8rSZ#U&}Ss?xJMS$`)V=F#@dd%*KGVaR9*VH`usQcmZ@BViq`x^2NUo zF?^uw5-k^|t6oM-3JI>>I?O5Lbf z%&}g=`&0Q+`clI9L|K5?7n9!W;Pomc0eTI1?>E#Z)^YmEEoF?~&$qmSse9?EK(k7- zOfjUXTg0-lDVPKTK<9=8=+G)aR%0@}j%VcZUSCGu`9~{6IqAExypaGV1cD?4M)cyH zfN^mB5_F8fxT^r>D9l8cIDP63wTmhT>cr@@BI>Y?oqGXZK<8!a?@dqduAG8bs0;OA z?a+JFC7S&SaPZyOXMycE)hF-z`y5aT2oZs?=81bc`ULVUA6x79#;| zo%$k0KSMPrLbiz~9@G%L+TQQB>9`yBuQzIYU&5BWynmxV=qc`W*sTq<`uzjgrSUaq zp?gCza9Kc#Y+L>KNI?)-)lD4_(@L=Rx^&5rcGF>j=|2<>{#9y09fTF}&P~cg?CMQ~ zkJRTNg$C|PFm7CM!DG>4G&;J{EF7Fumu$GdzTKf?Ipv&GQ)D)u;*1HtR6Msx^6Zix zwD_;kcyDqX%1B91LQj=P9z$M(nt(B_C9G;Ce4YZxUzgGe@chwmJ#Z0h=k3Y1E$pCR zg6Q}<7EF)~myCXhun?x0d@CHVAN5M=jE)e3M6qLgk#_K@h#L{R5CMfRD)881oIjAs zc-(Nz_chFc$M?8h`FvH`{&m53Ok)&pmeH6sDhG_A%bEtd(rTqQy}QeuKaV>MaFT0J zgM^q7IdPb~Y^G0+kgL7J_Kd%V)(^V(yAQv1=SM-tD2=!jzWr(6UW%M@T%^swJ3nVa z$h1qhT>(`8p5ms6B~i&toST||oGKYArl44#%GTk*vGt8^(#Tvg-Zb0}Z z1X*K%2i%DF!~35gzhS_rE|7rWq%leRyWmdSMMw6%{clF+K6vW z!_dJ*HL=#Xbq=tW2C9Ak!J*8{8+bA_WaWlnZNfg(L0VmbHQg1Qq+h3 zZKBMH6gwcszuXJ*E?C{(3wyvE*+o9fINSd-vziLaP4k6snd#^KB2ZUtLvw<4VqX+&>>*& z0qNyQ7X=!ioZ~-G{AsTjI@39>!RmpU&?ZRR^I~TGol|ztN7F6XQCW!^3NbzM~`SJB5 zQ-Re~%%iQksqj%0{997$sPflA3I#IOie)eGtA{Md{kZcX%@TFrORF)A$Hlk0;kcX_ zANGgdvFi$OTJhn~kkCm2 z?R@SY$qzBOgerv$vjY8RJSYo zt-EIsVcufs4x8S zVZ|3=hwt^-!Exhrd&!oWy3=BNLL~?Enps{&xxxFI&MueJv*GK%NZtJ|Zdq|@E&0A( zi;-Epj^)$xR5AbOf$#M??ue~S_5IqsdHs_7zD;P?y74l8p_cK{AI=DCtPv1Fcp$v4=f+g5#r!VAmqo8C-WD#diiH_87?@DJYh2h9oF!9DCgu4pwJoH`6H=5O z;v+^9GZs|T@zX=1LjR)*l_(J*y@s=KhTp(GU&uh7)3pT8-)0s`f71!xVs_ldVwR(b z=QN%zF$|djl-*1s;s~l%(M}_QM0c(#YKVAnV zzWxgU4}vg`22k#y^O9W?g}@EifEi%iVFZBFCT)t*lpBDO7I%UY&T{i{h^C^bUb5=M zB87sVj|UJ-VhV2SCC~njNnqeMS1XT4fVMkGtY8!&h4#35t*E#=te~94J9%gVQ#mwu z@;+{d5zn6W6-o}Z292zd&2!wJ(>($ATWI8nP>6_ysuP_D!<4OPrE`K`9z5KHR|uX) z)md7HfU2m+q3Jgh4im2pw7($`2knpK$c3$wf?z`iq>(!Mxti z+((Y-+rA(@o-!3rRf_rXeq}MDg{3cJU0P-EnAdKqgK5covxUZJakoDyx`5A5b8EY=H=^|7zv;)|?O*K*rZ~rh08iw=q$pj!Dc@s(EI&*WzH`gj_te@F_Sz>7+u6>FXoxaLT>uZNR`U7 zY-!w;X78KRxCKG6<#7*-3=o1r?dt{^m&dgdI6$Xzc5J;ZDfs#I`nEOI=E(EjG%!gT zpHhWiqOxftJ$ag%mPa$({{a=CH}h6bh@ zX8)K|u(NdsB*tMQa!#~NS7Nn9;_cX8KxWP0H>j_$bg&v(xnztX(j#3`Z=Eb{I>1_| z$UUAteF;(Ts4)K<8U&hBJv2d3Wm3&?^+v8p@pRY6FCO<~u&0Z~<@0(}WJQCQ_bF`Y zZHfQf?UQjvakcl!sOJqXBkp?XmkaaB;An$KHYz5m#rqLGUb@cJ=TgLbS+y+4nXk4i z0Kjzw);GfSpVMA&krwBOAiI363nZv^^`VPGS4w*9N0#uB7?4ib_tI-@Ob(jx$|)f? zOH{7&2ZWPND58o1FI7@l@rd*X-xJ=^@Fgq>h_NMsfU2E+ZNZ>R>kFcOh(+9}$yg&C zN9M@fCG2n%fiXQunk+&DMevpWZt3u^7W2^i@z*i`I_Lr{G0~WK(umDa=-Q1|yQkH- zQpokulsK`#i^*4(%@?$4pde*__f8w93-{A}`cma#@;n%VN!%$y_j{7#LSNm}SnhMf zY4yG=2HerZ$fwBnpzl-si%jk9c`Rpl4ekb5hv2yxpv4m+UbQw(F?KjP>=?V#qj#-A1q+@BB*i+EHvsA?X4WD9k;)GNS!d$ju;~{y_nnzny<}3MEyGUizADH1`39 zy^=oMIK%|nH&LD5!Qh#GdZ8Dej!pnfzSr?vhJ~g;)GXLDU7S4L1A{KNk*Eaj!dfU4 z-4bngwH$6JE-T2~!-+FF+KY9E*@Lr)uQ<0yS_W69E zUfXc*R)Br2H-VQ-mGsmUKSL&Eg8}l)xWgB^*|);Ww(NyuGQ+|-LL=HU5*30dBPV2_ z*0UqYckgNIFg$=e@d75lJ zyW=G1sCwa=8_tIJLxi6(zxB+u3GCd|a_`kjRN`YCZ{#OGJG ztP9yt1SA3np#X^Y^MTdgSUH96^o(jzi`;LL$A~jnzF$di%_!8bD$IPeksq1hJnHbAp{<=o>$D~#npM=$*s}V@*L4m2Jq%05u{j`SE1xhe zLab=JtZQ4KU-y$F$c|90YIa`RNtD4ieFWLIrtL<51DEnRGFz7GhxHx=8R1W|^=$Yj z+4LW=1-=~{+{V6M^JAWkfW!V&PYfDs^oC)TAGbgwb;?JV9@!^B~CPK zD-}3v4pNtEv>?NauM38poHM-IlKL6qC_joDhX|+4%Y<#R<2nCu zG84M2R^B`qApesB^1&a9>tM0jVEAc8A1A?e>D)g>Z;ub|~;3FJ7UFC#BW7d+owY!xoL7 z9>tUE^Gk^P`tiF~dtw3;__p;(k(f=#GiW)KCVLy{Mz~}~m^f9oiy|Iy*gRF>uYa6_ z6~_-cs{_$EEZ^<#Yacded!p)1-j8K?b2va2F z(zhu<0G~_BKZa#+F?sC8B@EJn*D}r@G)I+EtBbZ#g*$r#Y@NYXiYa=7gkknLTh_q<4Y)&CDPi0>JY{{a>j*gQl;rT zSe!(d9pHvH(d=50v4`bGuj7E0SVIDyl)7|^c{?iGFAheNfEUt>@SlwY-22HDND|oh zP(>Opdt>!MVwYwK5Mjnb1WeE}>XITLm|eO{xxH%C*snPao};nQ6Q8(A=9jxiU=yNu zSd1M`RTSUwo)80n^H78ccO2wpfTA}>4cxEvuV2ZlYkxhbnzxrvIWIjYT_!>hP_-*P zYFUQT)|Vo`-5~9yyt!c_LwJY#CA*EThm?H#wetnW5z z&(G1eG}Xk2Ezk@cs@3CQ?yY~C`lBqqLZbDqerv*>%*4&H`_PnVunV^pL-dTZ9#2lh zAv!q{hdCFkw27|gL7{k`zpx8gePVwO;6DXlum3u}`>Qnnr9hIAVqi-2%IyNg{t$qd z4|wf@(_Q&DpHHgDqbQ2upzYV=dKP+LxY1zM^Um&8bS(+C5wx;4kx)6x-Ic`E!lDCe zq%3Eo%qJUX*?`*1)I{=I5YO)jFYZv*=cfhQu){1Qw_}Uko)98>=!0D0omI#y&be?G zr8c8Lr?b$T!};QwBTo4Op*Wbc15R3B+cd-Rq+t=xto@`LbSyXNzTsaczP!{~LjLGa zwKz#87pT>r;}^#o{ZVp9qqMbor}0bkR%_C%KlQ?aM43TEBnf0sv>9%qt<_|Or_TAdLO@ty+ZEcMF&$( zBk~Q&m1Z=SOkm1HnWE9DuX@Ui-aoHAxjX>zMLzFR{;#x&|B|!*pFDPke=r4x|BjpZ z$1(W-fSXX1Qq@ow|G&pgF#UV5A_1MUorETJt}RF#k!*{~wTv|6KeJ z#>9Wv13LeU!sWl*`+rWt|F80s|3^CJzY&A&5CNjFSC8Rj=Rxol7NH1w_7TxIg)A>% za6>Q>y=f!_$jhG&H6S7LeZ&?p{~W1(~tUy8MTo2XF&+%&oJqPa__MZnRw#t(W~u?DN= zip6G~X&@&Ajh?r11GbZ`(u;glDrnAaeFcW0N8JpYUAsN{|0c};X_f!96yrbg>OWD? ze~KLciN$|!z5mG+`42?F(Zb%@&hftj5dND0=YJR5|3Yl|Z=8hx##R0Q4Y1)qB#{4= ztNIVb@ju`8zXKco)6M_&#{bS$WnyRkf5B^LR@1UZS;hRGozjILp|+wQsUyrWsI#)< zYH3fGM=yDvF6E)gq|`(yDpMI+mnh08qYURx zW<_+gP*6Dz2rE-a$$QS{X4&&KTX6+>Dkzy=|2p}wJIw-S0jHu(|Mp2?QWhI1j$h(R zpN?7OE6#oH2nlH_AlMU;tmbYqsV}`55c&De?%X`0KglD%IZmtL4Ix<-IcPM!Xt9<;yQ5Z^z=mzYf-WvPLBzG@9*zA{( z@ypc82TDKV{Ej%#JLl-)=JRsUGiG@RQ!pXM(u9 zfx=YcWm#&fOPQ7Ic>{yWB~AIuIC`34yQ1Q&*K*|rB&bh!4`RxbggSu^{V^6tD%2>J^^X~UGYWH1J(4S?J~*6J^8+jkaW9jkIKWjnX`}RVZ%DCb@oUjZDH- zDUM|^NqN^M@ut32WCp~wF288iMgp->YR0osX@-ZLvSda^Jnh&fPI=2Ffn{)wpk&oc zcE*aG&OE7AM=`ZkR`KU)49l!GHRa9#$+#V@M8Te0!ZE03dYP3Tbj*oWWtd2M8I9iT zz+Ne=uS?}&hC7}8#G+H&IM2AKoEC@uW8XY-quhg79eQnYT`6-6w!CO#-Ly4LV`3y3 z9SI$IC7BKROWrW_@Ti1y{k7o!uB`KTjm%&|pTfS`S#jFRkV9U%`5fkWWGPF}1-qC7 zF;pZ=WD0R8BBUfi2D4)rb6-XP4Z5*1wQZZa@v2p7^Ikg?+qJ>GNCA=vJ)~e4Og2gi zTmsN65FD5v&?cm(FBAn3=$K*P@tS0%kJiL3H(}+w4aB04=E!Yyq2p^WQl$?=9ScqAkK=WXzB1xfv;%NKF;sWAm&Z0yUXLmV8w^ zsBIaO+xmFh1#N+jc$<8ccH5UVn{K9B7kw_RT|dPo$HJ7_T2G-E+1{>FTj-bI8fw7p z5f5wom%vM!)Ij{uP^gM&Q5Ul@x2+t~Y17d1k5X8t<_ESvw{~q=Zz$}9+1WVf6!c?F zfjO`AI4MpGBJZwi3Cgh0tEO@v{K&~6;&*tBdwgSG`*vgp@!|tkImMH`x+uOY{3{D2 zONwU|c!OIQzBP0%F^0bfv=Z7Abh7W=we?Hm1-F##ma0`}KK8@+Lj2jX>QO*F!Yc~@ z($hif4X<#9$XNahl@|wTZzW3&YI$7yz?r;siP7x|7v3+|1Lmg)udb*%8@6%c*HDA z^stMDv1CNgSs^Upv3trS(3TZ*%kov~-^gr4V67vprIuqRg(DNl&fKpct zoTjYOczmJGEtjQX%Qd)+m7}Wh!&I;prd2Q{sBzn$1CxFz>;0D0DJQ<(bkAAq-4~Zx z4OZHFFTu5oRMa3*eUmRe1dD0l)k5dU$!ekqkHVNyAKoI$im1xhckh7d2+@oQO76_&S5F{`YcEx>^*o;_EM@NUKfM2s-y zFm${gN+vQyj$5tIclBF7EmmX(1^aSXD=a8P$w1xOT?ZHzCxu=J&>%I zVN^IU0S_B8Ok-``Z$i(>`aI--V^!ywol+zWS^&#mZg)or>CIs(h&8j6h%U7sBoa33YKzJ2EhhjJAaZI( z=Q^l@_LWTWq4Xtg#*l90{0hD5_+kHHSr_ZNs=(0t__c;IhJrP7sRFEvU163JO+m4X zHwtfL#Ews(K(Tzr-S=qydJ1MKr;5Q|try^^`B-&5K02?l=bkZpiqO(1Jd@c-tbA^E z(X40dpDk?CvrJeig^?$J<<^H08;qQLUm}oK|NaJxt8zQnglSEZqRS z=0D9LkBrvu;X!XKwIhSpaB}%~x@k6h??sIDTf)DK4u;Pls~*1BnHsV{GY_Nr3V*+( zrEJZsd=@C&FIcxSF%w$c0H)Q*PyZk0fqy3UAM?O}s0A?onH>KAk_!-#kyQ~?{x?O) zzbB0UoIpZ-@U#8Dv&(%@c>QDM|G_8s-=rY_=9v2<@A$|4_t<|A{Ymq$w*DpVqo)5F z`tAQa=AZ6AI=guGZKg#<@3;(3}?fyrq|ET4^rv5Mg z|7ht?x<57jzjFV#tZ@HV7XI|i|AVF9HU78t-tXb`f4gG&pU3C_L^8+rp{DS+Rwn~H z%m1X+`9T`@4=sp~<<9Q`{(q3_WMujfX!xfV#K%zntq~#qo5${7SmyqlZYSs8G7taM z?ffg^PkaAs&BFNCnE01P3lkIThm6JlalJe5>F%jH|NXoy^l+sRW0t1;hEX4 z4{H)t%jGBQm^nlT8M3wU2c{VtyjNs@&{y=nMmb|ATkhW`Vsv`FqH_PZXa1_&`;B^` zO1Ua)#n*7OFjBLzhv8|%2;^-7!N1(s>-(WwbdBJEFqyM{I|zO^ zYgPI3b=l|$*Bi;)&h?=7-P9E8oZDe>{e5`R#0$}TI2~e$p%sSFAyuE}YW`WSlh&A9%60WM>fJHpuH7*H3u2ZfAx#gFq~U`U0IUJe5C8 ze?1WBAp%`*EzWX70K4JwpLNCVrFgr7Eb<1wVF4|_c#H569!(F#+7YjU@~P~`)fg0_ zi+5oEjAnqy5z&M==*n}a!B>=-(F#1qFe*t%WCUfdbpUiz5t5Oa47jnD>Hiq>2jKcL zQfK4o^(q}^eP_MOdn)>@I7kqD=rcanuR#!QjbT^kvgdd5l#&Im78Atqu{9CRm=VqN zbCMC|LO0hE%R+yPIy^~B^lcq5+fZQ<{{4G5;@7NqDFfcdn#DjLTNTd_USVJ>K@X#85W^-k3{t;X}lVG6dZ z1~2tI>K)}TIaiIN{8bMsyQeC&Dz@n+={gKPWnaB-P23^44lhr6q%bGTpB zc%bE&Dp!P`(JSO=8RB%dJrNq|4Dk?A!Rk6f3NlK7A(B9r@dfAy6n$60s zT^;5JOR&hI-hMqWq5UB^x|Yl17^W1-{t~W>t4DyFp?7H~dl0!n2j!u!Q`wot&=+JA zPZ-6X6k#-AaSF(|PwGF&FAYvbD95^}rqWOMBR@S`NY{>AJyARUkYStuRHG4ZRK|AB zn0{oeRN<68Kxsu+SyEX-2e{+5bc6qnwN6aw{g9!^YPq^Ipy~ zESh?dJEA#c`~{rY)PS@I*j|=wK(0o&`(%f~0Oy8wftCGf^03Dd>k=y>j}?-3Do=+& z4q3n@J)0?T6xw7;RmC7X^V_N*o@ul!;U21%Mm$Ep71QFc0W*>lZTCm?68h`!@a!X&n315sl@EYXT?kG@6fF1FbHbm*Lr% zd-TEJVwJ}YL)bv6TR)&XSywfVjkInSY>V1Og$*eoD*F^ieK-?(5N3 z>X(h)KO}Vd8iJdThjQT`>PZ4#AW-pnC|t!ZqojD>4N`+Tx5i9z2MKC*KUIPv6d`h) zZ?zc88pcU+SH+DFL+$76)UlQdAai5$%(7x-?ZnI#eaMG`&a!D}+QPB%85-A_&`{HZ z#4UDB$LiSIZoNAz;xr`HqQh152x`l<0orp7+JbflL>;PG5PjUr-0GNOj#?}Ysr8rj zx`6jdZG$;pBe}eR+3g@+yZtXAba?it`m2<9jRyc27Z(+DeI_EmJ*?U9uzpd5^uunLm_fT&9pSNS$CLS-@#ICCf(e;uCGdj z2WTYIuNUz=H;N?&pPk@UE2r({XFvwctW>_Al&*I|9=B8AB%nMj1X!`@=!7S;7m`m2 zAs;B*!UYJ;fbQ{q!z|Feji;S7Dig8<{@#VD3X2o(&V`z1OojZ!(!Yt!a_DL?_(>fx zU0_dm{%TvlAX|WrhOTyC<5Tj*f=v6DsBnhG&it z)$pPU>c^J1xOx=^^bbgI0`VTmDH`=)zPCQ2XDw5teVA{SB0gGi7(nN%h$lEnyupE? zP>?J@XSj@YNTfx5zyXXJ9H6gYpHo@Bc@hna8mHG?A0Fmp;BAPi1Io1ud(KJXvPDZ; z{j?*q7vQIVd}?&8XQO|_6=ZjVSyI2AOHE{UZy&k-k9b;dpeca+oIUEGIm$SCjyM10(-0q z-me0l_=#WmM@DnYwG=M$1OHU8ag%ArytKxs!}hR)iX$4R#12-s#6uMexj9O$#Of+b ztN)mpo=oJ8DvoyDap@0B*Hex6O8L?Z?|bWAIlugYGvJJ1Sn{9Rwd6 zsue$N@y~#OcwpMrAHrO8`x$Z@VHV;uS6??vX5j^J+nTtDLp2rf(Mq_p9JfUnfyi;yO-msK7-08eh-<*c#JgXlv^2~QfTh1BIdNa3kDrley@@F5&2k=srTThDJ3EG zi@IBw2P1;qe}<+^2mg-!QR?aXdZjm`(Zz7$Fabp?T@Q6x4SP(`Vh#ALEwwckiOG)f@o-JB^bDHI|NpwrB35Ymt zx<)%w+sU5Ti~d4`Jbp|V)(6vfyE|mEKw#^JYD%WbSXEtA^C=(q9A89SoD)6i5 zGG|-R7<0JAd3MQ%_sT}KX)kC=N>!(}ZnQmHt+VpEr#tnR4BdtpzuQj#XrCC)#@kaL z_R1bO7kY(Oi-Vu+4aq(VwBS-i^DMmvl7qIxyrC;iEAWXOLL1UFY7f73O)wdQE3Ut+ z%77kwH#&(WTzQZdPTV+1L~K5H7g}ii<`WRt7R&x3qQ_jx$(IAxE7Mp*`Ypa%XHlN$ zwoH3KFo4rG)ovotQHFyHNVJ#3aoen`22(MC`Y{ zNl2kkXoX!EcZs{d_A<@kHC6AfwYLP*;jwhOBDixcamtY@+AVpUS0iPPhLl3+g@Afc zO}6lTFH`UF2z_AwMF~Nm#`mB?|5TO4)_!rz>ULg?s}q~zbEk&1e%%G2R<@D_r^O{z z^R4G>eA=tQkdaQ#iB&c1ipj{Om1;Wd`aGZv2R>#$XK6iLWHrbrLXruvz8!m`M!6Af zLc#`l5i4XHh@~z$CXJm30Z)55@R(4sfvFU@-xH=yMy*6L(ON2V9Cf=viG`5H2s;iv z_YlM&VqVL(=-2eCP=~vpO0Rs!N}%bT9bO{$vEKqWmPyXC&@Nnu3(i#%-rq`myiCt< z-US?zo4IFL_l)%|D(oq)f+jS5g64{vbQ8Y8`nz75}mW55J6y%QjA0o(= z`G4BSJt*yAhMGB`0&0Qstpv#qH`p;8S`aUfVAks0q3&|R(JnPtP!EHy{50ER4_Tl> zC4T0dJhoQt{;;hwQrWc*eg1sw({@3h1*HmAQzP=MD07d4>O#BXwY$B$@skiIs^IM= zkDMQcNHCQ_tf_3;*}Q`WI}1WlU|xHlFE$hM{b$Tdw;-ifvbjucOCgt*;+U$SqEVmZ ziysIbOv5XHLXGOPIVZ6RatD+`}Nun z)ZKnkQm(@a2MSfT3^-CBHn(4(|E#V3AR_|;k*b0RU?@@a~ex)?D)*4i`tSEUCj#$-fE|~_ljwDc> zU(yS($`ZoSZZjXvB~5QwZF($qJn7$0tW&jKufCj|)8^6&kfg@2E3aC`VP*Vu9uh89IYF``%(ev((L} zXj~$iSg`5_k^!Qu&?LlEAf_4Z0xPdEcKewW-pZV4Nq_`9#0WHYsO!t+Cd7c6XP&aImb^ymw5FAHR2*+OuWf>coT{oNpTg@{8A)Em0=ek8=%$C54yfS zU^tL%P!t;eR{gDJo)8G+`@nrg#6RV$GXf*r(y{x)Xe1KK-`eX4O(x$*vl2QLy_>&I zcfjqp^INR*@VGjyy|`lwyO#f|8_ax>ch}*p;3+M)aL`U8A4Qc5?HO6)rqk_smK?+9 zq&$<7Qg!rkK{kiNoeM@&NCH+c7Es1SP#Hi}fdDJPktq#KVW-Vc%~%Grql;k%DkN?b zDpfgsLB(WKfp%ofylF?LUu!cS-nP~ZVlCSn*R69|UuCSZm~WF>DF4nMUF+l)4V`*b zF{C|}87bMvgC9#4ohpE(2p5l7IduYp*BU5+nnZqrXdCq-cx~5aB{lkn)RJva>Idz3 z!C5sq)tP)|RZ!+9^kgVYQtj+U`h4Vu1oWU4oLG&EJ!yUE4xMGt`vE{u-Lg$k&gCjGgp_I5~jT{m5A^bi?oP z-osonG;*4z8WiLH>aZ8&%@kzgJgv0bpBpeW(hH7WA}yhBO=ag>sh9oFQksN9=0wfD zi|%T+$m?e5(o?x6pari+AyizW7*xo)N6k;#q;@&X<4`|F#O;A8y$F8LnSrV1pSFS@ zO@KI?`n3c!!rYNXV?_~yIa{CsZsCtR(+r2=392Gmqt#!Ke%*x(MkdfsX&sjH;1KGR zW9l?|Z}m7^`XoUGEBblIKTp+4nYf}RxwW4yY^>i$)ALh4pvmInU}9!;Tm&pt@YzaP z>`9B+&qt^*)73o1!Ra0x{VdPov0DRclHzg2(zs? z8+qp&Pr6|Y1oi%)p*;R!lALA1<+P+pB`fupw9rD9i1{g0)>6Wo0CE@{L-(|Zf&kj$ zr}$sxAX7lqpc?i;Xr4Q6VRRlk6Cp`l0b4ApyAm$$C5%|X!4FpgOq^45rDOK&w?$K` z5OJ5q3KW?_y!wM7zxp+U2G|V{vm1ypS37tU#DjWE#HC*dGNgm(lPeX`V%x&R(U>1a zH!JX@UN6CJs^H!wtYA3e#{H9r(1pFDCIr}$u;n0|hJkGyeADePn?f^;?n&5pk!`N( zs*5#o);+!k&+az9I2%a z;aZMCV0lTDBo<6l=BB&bRn!344MjB}i=v*;nH_V~c!FvzwvYw|&6RbMY|iJQ(V1Av zFTE_y_B@eH@Q3;Qe)ThXf~^8K77hC6E|#m4|Gkkl243`ac`LM9BURlZPQQou(o-wv~nmu+Ueu!6-pkyT;TdP7gY_4I?5~nwi}{oe(NW z2qY6(h|Gb%xCX59=vZ91z;xY<9gj2;DKd+-M1u?oS!?9@kWC`)D_+^#XQM*pH@bvT zL8d_@)dX|aG6^qHdb6~E;6JrA=9uFNmlF#J?X9Ejl36=6~g z#-A-13FFL-_5|7lBaB`E)b>HF;WA9kW%efDwFr&Nm5o&;;6bOLhEds&De|Wij&bqMLW^30f?Je^@iZtWHz~<;qi!7HW5tn1jeDB! ztJgR5+j#|pTJFVL>)KNUV30#{WL5-pa6(B$N?rY8kt9Gcrs zdoY?HpU>q|Nkq%c@184IC)G4f6UD2(j@w+s58hnLESeYk)<|HhAarOAF(gs6wV)`( zl63NX-$4RVp>=ll*;OS>I-*}lX;)gSkVFViPW+*N$wOjYxmETGP1^3Y8I?0y|8ajR zt4_WU3}$eSf)*kjH()%f+oV^x+oT9|*}L2sYk>wOadTfCEd&SHBCxcl~F;Y(o04qEY)6 zoR3RdT>4a0O5^_Ghj`my0K??}=ImVueSypl?BV6bBJ*uhcXlFpws1ljxbAV&!I%sP zEqDT@^%(rFiGJ$ST~b`S<1yk{zSOU+m4y{)dYAV2BeojUlUQ44;Y4y-WC=ZMJwM*c z@*A49@+vF8*F(5c^4U3aJ!h-GKhD94IsII#jZU-MO}k%G7A{I4`cqZ1txd6u>w7)L zZn{bWdxfRW?mpK(y-OT-xh*e6-td$!X#A8LXCi)?@N0EtZoT?GvMJU{=7) z?wv0{FP{%|sbF)h%rxHF5E!v)4^#y}!Ks|Fe7(&ehY-L~tM$xc&q&*)mL+bj{L<(z zQLdCToZ?Pmdi5%&Vn`x8oto%V1KG}G$e!Rj^P?ZOV~K{>sAnFnW-X((L_Y)^>Tx;2~GeBA0_?8uE=jpB^`>4rU z`-NUumv+J>V^>xA5Li!q&O71ERhUzn#{wJ`?+CT4vmpD3ETZF+9-gbW@$U} za~Pg+)&r%kCO;|T+)&`EMw&er-f%fRvcpA;{ODs@6ljH9^6m!&Z#pK$CwT493_(x^ zPcGgew|lF;lc;n0$f<#C(W|DW5|h>qw4TpA^(M%MF?S!TT#n4}Q#J^82YuPE8M#Le z6{QsBk42C&XRNM@nl(KwqM*nGReYXW2uku#8E|9wD-%-s4HG(Z3e(J(5c(Xij8=mm!}9gkhv;q2E@QwyQt9vrPkuR5`GI%a^aHPP;TK?s zEqm%mgHd8YmZDLxEF-0Z0n6 ztr3i4u_ocXi1pb{wRc0`jr7!2NE?RJwHeUjqm;S2$l;||J5QD^lju9|xOOSu*@I=F zz()>|RZs4J9-ZyIp@hWaq?lEQ4Z>TkyKLT7q}y!F@2PsfAI*;V+}+*+(kQc&0VJC= zOKIYT!G6yoGlmH9__6}!&JhhF%{S0huCB}?GCc?99se(#98 zw;QWnN!ULfKsDusJ((*anjc?64BdMHxdH&2Rg zp3Sjly;6)f#!zm4fB4vdCp0!Zuu!BfzoI#kU7DiIDA_e!zhZ~phd3ko4XK$36(8w> z5cuQbCykmC?lM>7Qv~IRTP+VsGY1U)Q;pEI(c8h&BQD;^VM|$b6d7%9{qnXdtJ3}{ z0bUjX`%$)&Y>rfP0ps3Ur?UvaQElIw$ap`~zL}#6*4vy{a_5m0p;H6n#2s}=JG+!~ z*U1JfrzA#*QK$*zq9?DlV*zkHE9nbCB{nKJY4(QEr70F4fbtYCkg{+I;lWx~D(J$z z2cbY0PGtukgm9RdLDXs(6v|o6&sCuHJF1RLrssR0Qt52_n(>zDg?kv=&1(6uq5F1; z%}s>&oH#E<7gGsGm>rV#dnZS1cv&qS&?*g z`MBsyg#fK`zXPD^7B@UPtdOuMV$YFSNzTX@$f(R|O3CQJiZG#R+ z_9Vu;4Mu5a=1C-uF!>z5oVmk0G~b-2H@8CxftmLEEmpy>5_P6)M?>`Grh0fu?3}!- zu>j@)y~rP0cEm!&Jqs`g%s*L49LiPGrA_m_3;SwYf9nUzlOr*U*1S6K?K zX0xh?g+{jWl}6jOZ??3ss(_%lA(@Yf34IIX3-0c*&1HSd+soLsKcC}E9FF#()l3FxAE>8K(EoU|#IS$ZTAV*)jacYbxeL-=QcPtMa#m9creA@(D?4gMJ8 zW*$>y>rygY3j3JR*BQmqc07KH+KH|6FDBL+087HMA2IV9LsU)#ENIqV3l8&uFv?x~ zy&@PSnKUxV!)q7c;FPTL@)AkI*N8}!My5&Zi5-U0K-08J%;x)ZH=>K{4q+CHgEUrC z=t)h_i9>^kt_Qw#HR^t@)l))$G^Pj?| z$ugTU3lL~Dfo3ra?k3ynj%br6nxIrd^2e!63%*F*2^$Q*z8D#hzqU)4XacB|w%Uy zUI_DKexN2DLZ&dCQFsG-2`a~Yv85GEM9dipkvA4D#sGr_$D!1BiD*-zbp}+mc-%HM z;h``7GH}qI*Scp6EIEW$g7{%U8Pwlkw}v?of<0Q4hOdahS3l0-B72qltt&!1GZ6IpCywhxR*kHefUR{ha%`lR*9c8{I?|7+8Zk>D-UY6L|BX%M3+VzhLJyV zPsxKKqhxpNRy3iYmqC~lFDZE1T{?TUT|BS(Sr-*br8Q#vyg^f-;Yf4Bw2; zMork57b+IwcbQgv#tpVLtKj;OUro_(New`&Y_dt}H!JJRPZFTSsfWNBW*37)p1XvK z^vLN@Aclb#a|qJhhTy)Ct9pXe#O0fpDaxzBYsK@bGS?}G3f@Jvjef06HB+MKVO8L+#be?G;4dtBY_(_k2Gr}z2;@$aFEuX=t&F{#Z&`vCd_!Ze2{i?rJxJi^EAILU zSrr<-L--VPQq^6TiKU+&XvN`{lJ93jj^+7;VitYO*CEA?1*C{X_t!7=*cuxCp-fry zDNou=2iJ*7ParvSuoZxPHCG2emXpi1`@mXqw8KxS)cxYR=R-WZZ_>8YTDxx)wcLG0 zJ*_Bw%U(GlkT7D;QS83>NGH(FuIc`k&aN=2iWrv5izE9eg&4Z8ZlQDdsFw;Qup)CT zXKY3m^Jb8VM_57^F`uQ(wXsRJh3gIu3xI7G1Vr&zHgJYa_uGYdR8Cq&PrhW2BF;v)o7eg&Qx-AW|(0-l%Jo7{QCtu@m0w*JJp=~fec|3N6YI3ccU%8 z)BfTcgy4^;Hr+Qe(q@T0}Jfn4bw zum&EL_pm|V(-7blM9>Ceh`{L2GqVjeoUh8%qLL#+>WPZU^-@Y>s7N+4U1I6L7{KLZ zKCq2!TMH5KWxF-ZYBOwT%%B?HG2G33ADTONlyahp5%uhHt$kPyn%xZ*dMkix z+8O5RpMaflO_hO{a6d5X1k(ams@fUco~^QB2#Q`KRK;rjOTX3%=NwIXUcV}k4i4|j z4j(mdSYxhA^-@nkY!~2szk-UN`=0?SaJTTAaq%PgJlnmE)=Lg6e&d=pO72KbZnEL@ z>))|-i*Ipw6OOt0JF=H~=ItqfSfQ%@R3oN3zNLePQI}^B-4wKQ-T7eao9#SiiDC$c zV#y*e=UVzrJe`A~mL|tu-cx)j6S**u{Lr}o9BZ$-pB_k&J*gOI;jXjaqmN*Morl{X zZ%F@gU5LR_$yh2<%p)ejSjFphyK2hDC+M_=bh-iF(OWw?pW`)TMVI65hp1-l&pNtG z8(9`i8sLpqpETSbYjFFTo>I?k&7c36n5i)ykzw>f7s|5F7T}oP-8{aj#lR~Z+)zF* z0M4~Z?VfzD%Ay$Bkn&KBuZd==n>*#2gpJwQtx9uyoTb+3xspLQS!wa^oGatbv&i4! z^&A!p9fy8H2NZS2dmn`p7gZyxPioCRqr(`LBKxGNa%FT=x}y0xwkmK)Rd6SksH=^V zP;U_2WmOm$B+Qz^3sJ-np&Ez*N}irK42M*`jqb|mARPnJmm5C~H2zXl(WqUq^mb(A zXMapV`d>e`#wSC)Y>GB%7E+NM)_vy0ve;jIh}m!>e>$-k%ua{Mh!H#Bidu1D&HgA5 zolDxbp(((doQ_lZ^PQ5(lJ%ISw4+ztJShHfDsQ(nJ}yMyXChFm;V4b$A8~-A|MH&) z4wGc=C%P@^c3N=9C6Kc?`iPDa?%9|@Nm&{KT$Manc3Bd^E7RHQH``-wV}o_va`y*v zsIq>{udUB~_3S?Zj}AJ1f3@A7Pg{h7H>6=$hMG`zfQJp15Qt zLm{0C`o{5(gtuRtyocwp7Gyjxcy-Jexx#+G<9OfvxVldI-IDpYb6Te$(nZU%sj1?T6!u7YdW0iz!FB-( zYlP_xninCGF;wOp641#?<9=K9!$2olQ>H(}xO%r>a?uTCY zU5rvNd^RP=S-iFt4=9*?j=M{&mv&;sEfxz?Q!@0%*o>%6#LQ#9Ca`n($y73B$YaVD!;mX~OS>=7ZOv|@m(A)7f~f$j*GtB%N3bKb zCNs6A&T1^ku;;1YBxjDIHyr0Q^pBd%-Og6x$Oe|bl6b>4UV(RYO zYljuAzsUfD^UY6)Dvn^zO^qY6SCQi4zfL-Tlp_VdVMZe}DRtDr9Nb2E>ejbu@d)!V z18siThdPM*(oz_DQzX-)4s-wr;f(&sO)q*72O2O1t`_KhH=JQfnq+N&_0a^cYS7-b z%f*k4H1WkparGA&5!D<{%g8Nx3at)%wEds(pC<>_f`mX{g}$8{lGZ&#KtNZCF3|8@ zcwDgv_)F@$iq#pWIkaCWU|pNq!aRHG>qpm8V_XD<5)C87e!|cO?KI#$m=@x0)t!v)CVUm&kjUQ=;9?v2r*HDF;&_asOE#1;I!l!nSb8^7tv#V7}R8+@oj<1ZT z`@M}WMTka^+RmwC1jwK`!O2SFWWW;|6ea&-LaoAs)?m1CT%{tLqc*CZuHNS^%0sFV z!uaT8|BS74brsd!vvK51Ev?%(cBjN+9_@lb?qY@}|MgIp7OGT9L#8`n>d4_3`8+if zfjr8LuOSE_`C(c%=F(}NM!3rNk*kwr_05QUa;x<_oB1nv&rqz5GrdxqrZZ$KjmcE> zucXFGVX}q0D@=fL&uJQ7SLCc=k%iqfm36>xr{F;$pInZZTGdL;_jq;qk%((a_hwf?k~WX8)lmhR^&Va6w| zd-g_pcvSH?`x5puYy0!pw}wF)18|)(4|z#Oijc{DD=E6^cz1h!VU1V#Wx4QEL(>tPI>^5}Z3`^y zMU}3;6d@|WsRrK*U6wlIr3KM)a96>;KLXCj^IIn~-!+zN_O7UhyQY+8dUwl*_7)cd z`TB19(xEIAd0Qh6&(taO4*C;lx0^?}>Esq}w3<^(E`s-YzWj`OlvOKwhXWPj)nhpH zOYG38k0Y)b8&w-|v}Di_8>^qm4I*wCMC{5u-)*#2QQPOaCi;#KYc~VUW#es;XppJO;79! zn>tT3_MR`tsuk%p2S7TCb{dLy>hJmHHxf-)?03qEc1k1-J(1SloX5a+8%+oUJkai- z=>^|z4cL;$4X>9-t81YN8;O7JH=}I$(dtVr8J{?~fIl;u2jN0(Jt91A+q)0h$Tk2H}Q(!5I?| zUkp(XwCzjS#h?dM{cqb*H@&*{1+@6Gv8F&mK>u2dbjfzD`kpxW+%6MKLtgec#{<2j~~v$mm2HBXUH9rJ3I|%c#Fy6v-T^UWhKp? zj~w3b+#G&zv^-u^%+h%J;umI+n@@Il17zQOGTz!U-d-}^{4?GPI6nwUUr9M%KRlxj zPpUgN$-6hn`!~%RLmK?=rwiq8W)rt%4o@-Zln?6PXLZwOb(3dxvt~D9W;ar1Hxgzy zGG;g8W;fDiHd1>*?4Z2b2PL48MK zQCl0Q4+x8(kf@Nxzw$@W3ffv3{RgUyk%95U{09U^#O8xS!raF6H>Ac;(bihu=ASYD z!YA>8zWE2A1S1pUKVfB1%zs(>R~QpB8#}@8&!4!z{j49f2LJu@C&gdxzkr+AJ}4_# zIavwV7?}vzSblRXuyXvq{yA7#e}~vU%3x>sNXhZLEH=j9E&82<;lmOK$8S!I|9-Ik zw#&gp!1|Gjm4lIhm7SGZ57Z=mNV1ooKA7HMJf#LRdh>?-)!@}=@CTebF%=Cf8`snO` zV~F@W=1;T#0k`_ydmm%%4@bm@o!=O)4l3t4kiaHHSEr-I%vyBN?R?|3taJ40p;U2-jgIEtH@+z~qQ@Lu(FhUSiqQ3r3LGV3%sW^0#%!tml=ZmiFr2&S01U%r3dDFXYcvcp6a zYe9q{sjMgt*h*EgL)nLZviFN|5PIg0T&S%hQChHl@2ix`)aUN>d8~0I3qvA@6+I-$ zw49kVg}msP8DI7kgh`e~k)dJ5n8eo?TT(>GX!3mgLc!DYu!#-X%(}(71p5 zv%+LWU2R>S5|7&t2)1Tz(6AbcvGqv)GD7ASA`PfzB>T-7OS$o|- z!Oq2hl)$68`Hgl81K3&DPWQQ^^^Ujxyud4WKXd8+q$L=-p;voR=y^h$8-E#d;zchqO<%yNX# zjp+E=l^wmVmWNxX3k|{MLis4hominI12Zc8mXL3?;$~qdNwdbmR>||(G7UpyOBAEE z6xU3Np<28IGQy?5uEJQSdx2c%22bbnpRNkTK6mxeHcq~D8&hg2<4$T}MsD~hcsaLy zavc&0o*kZ1HoX(MWhSh*-&HKuLFA?R<@UI|!=`Pm>%@WZ$@->mcz4>Zx};hA4(V8x2bc3aSAD=`TOHb`A@oIvRaJ?`GL2DIq!1cBzD?BMqs zgn`>4J@&cOoJ!x`b)hfxfT1yoh##cr3^@jViC+a9mN<$ZWq|H@Uy*Q1M#UrVI9~OD zKU=*RU!h?ajc%HMrT*a5{CZ$@<;W3q{|Ewgaqrp$PdB*22d^pw%qh$N4Z{`rok;65 z)SVBUYS+er)fAP)%s<`dyj&!y=>dVQitl&28;3}S8g84>Re!4lN<&;rU@;0vz# z6E|UZ+-MoSI$C8Zj7kE%Hj-AGnf_8V+890T7&T>^8^DjB-!_e%edV;e1W;$dG89K~ zfdj6Ha0Os<-+q*_=i9)Bn>6sUWrkzXWtc*_-<03IpA7Z*={)Xs;9&}JIR^XgV#45V za`BRy{dGiqsT@RZ=r|N!#f=wq%1#~S-nQyu!DBn`$vd)Ra{m~Y*~P)(0!nUZ4X_m| z6i<#))z(l!Kc~!C1_Vbv;9%<>j`+HwpXc8Sb=Lp7e%HIH3-$ze8;piiD9lzjIMCcC zfBp0jHkz9gHkOg`)hi<5_&zLr*IuGC+0+onoUD~M7+UA3)mnh`IL#z&!DErVDsrIN zq^&p_`u6s=&zkwyY&+`Z;R?SqF{tJ?_p|yA9wd23bRnKDf;%~xC}>VWCU!o$giOFyQbb&mSJn0uJoOp-q53D z5-}lrG4~7>?bH@39gH4J1rb3nX@&>^7y5JK>AZZm?O(l)ys2{N@y+>}(};5PVmUj( zPr1kJeRb(fi+SEQ7sAYr+udJ{blcgN6({p|RDR9!4-9VkgUnU+G{3Ga#gqT4|D~9D z5;`MYnJEDc$&=W0MMv8UYM~|VlKQ_Y`wFPImSyeWlHl&{Ixr0G?rwqLZXpB+65NBk z2e;rF+}$O(26qV>B>9Jvd+s?Q@2$7~N!Bp)O?6lAuHM~yb=OxVmYCq09VJbz*j11_ zYT&-wgyLM^{EbyFOsBbqFD=<`u$1K|N(@^ph7jBOH`f#wN|Bq0{LQyN%5$m4%%J73 z{a?~Elb*VB_CzQc?Ar;-(U>+%0idL#lDWEUkgE}CkKIoz2n)CrJ%Xht!qH3uX!dQ- zX55v7d>OLkxWZ-|-xq7T6|-0e?@ubsn`9i8>dp5_wB!wtt?%hz(-bNQ1M^hwmNqS@ zcq&_4Wy%Yx%8SZf4UA@a$|Wzah|0>2tx>Yh$r!Q=p`b9vzzNtjS2Rd8Qx^;%_U$s> z&EE=~3|inJKI2v#Vu{Qz949F#Mj#gYTzOG}M^QO*OJ4Ti zz#Od;<6?{eVD^yCmu3}o!&S7sVbF7AXYch|1CJ6kp&3uX8sj(ASUL+29QJkL83#Ud>Ua!_*vI}_`@B2B^s~YRE6PTUr$kaTbm3G^Ex%2 z3E}{y_9i)-x?o!Pm#rw>^_~@AjO4eF$eRI2Ha}JaVf$6Zm;F|=6c`79(|v*C@D|YS94?h-6x4r!_SN(YDg+%r3}ddhyme% zrD1SRP9vA=AZnG4VKYScugn35)?sK|_IO07`7@FrU~3wM2H3S^q0!|t@JO9+-`p=P73=(GkSV}#*VLC;0$XVdD>3ID)I$E-BJjrUA=H1AT_{*baUxF= z>F&jQFIorE^TsW>AJwQSd=mb=#1Ps(8~?IvrZB!5me47J8E%KPs$*1bxle5wF95U~ zsMp;aHM17g!$BQ1(0!A|M*cw=XZmaJ-1uqnei3d}0KjDQ5cgYtJ@j_`@a48ltE;rI zOw`6bOmoBC+{#?+TfzPB(0HfJBW-a4Ajiemp3|S-T#tKQc;&!IMBVZ#S6NE61RWyJ ztgbhXi9ff%Yl(BvtJPxClY8zSvpWRUuvB;Z&n}>G@&X=4@&hANIBcR4-=szS=zU^!Q5fX zZROER+bQ@c)eK1<>dkv-=w4C1DqKqQn ztKWSq7JPNR;N;b^pYU}@Z~pYcce*bF&+hf?$}J&BJz-uAUY@Pksx}EDLejom+>M-& ze6#qWn#RzQ6*{X1-a!~4;H4>y5Vkihy188x!8nqSw2_QYcVt9Dk?8QPqh%UC69JKW z(4`M|b;ow?R-8~}=^R2-S303=jr|JKwUthdUfdzlLb!dG$CB~JyJiKi{4806HzKSE zqJ@4E-yG~swF=ScNH`$V$rRiIj*K#Hyj_%ln+Q}QQd(3s@adgQogSSOzCP!v+0^u@ zOVommnji7G(83^#;_79H&$U}$MZr?9yq8e#v0CDO2ph;fY5pFTP5I6@85ZieHdLKU zTlg6Dg`o_LUZEALOw$tB z5{YBn7eh@yDfDqGn(r4ynZg%BHuS$0)aswH2&A~k4#kwUIPGB9bYNjn+~;&Ib|Zj; zZsL~mKv<+tesZKE^nbI868q7Rg|HotFeuhw)6+gq*T}2=2OMvo)AJVEj#A4txhAS` z88$9)3y57c?{hkJ#7je`5cGjfw(oKfs#qza#YIDusk%mmgMsW9sidE3=$2*5t2EIt zRf%duaC%Gm-8OjwhlwF545&XkZ2N{)_*UH+qB@-`0A^vn7XqHL7F*|bc}3v zOyxR!=AulmalQ~EHO9jN7-bO zK^S$g$=o1$oESMkvx(NN3R#3oPdP!OLXJ;1d`^xan;ixSrDLyFPG`IjJL%;SF!iME z3%P-xJ$<%?j$oVS9`B{&exYWRZjTwtHef#1YyNz5eN8Ro`i!5i?=9AVLAO18BX#t1 z47^%ss=`$PfIqf!$?BEo+TKIw#A_Baz@}#yS=-c3B`+64SXQmhvlK~h3?A-DI`#e< z-`I)xMlKSZwNG3=>>r4?aZ+Bx3P_=5_69ZvdI`=T0Htjd}Ojjgjqs-hjT& zpDotg8u!R?j(_vV!hOKJ5FY8s`wZV6FhyE&td3Ghc$5HPhU*i%Y=F|x?JXs)8cbGm z!a9tg#;{(+%+8hYMv8dKSwbS_)vkmiPA9FA9dxCvOsMLb_$+QCWIPLjj}w!`9!{;G zRk`oh_-j4lNl&+3?OV+xl%QHWTaq6sYr=Dx@)j9>P?Up~Ug=+soeGx`$*aRVKA<)Z z)5mvIML`tuZ}OJQ_}T;_hBOvN!dhVQODHOZj5{i@au34}vE@uC5Dh`6T-@z0C97bu zlW3=<$>EJkTnE`u7wYe#@i${|cuqdgJ{HUjhthv$Z6Lj_)IlYCo^gA!Fjg=%+-+_F zaq^%YyEn8TS9D;Wkze9KSRAP_EcVhb)UE@Pc&ojWA$Kx`7?EK-$Gfu_>wN%az2ggQ zDtNJeHFhz-p58nrC{(rtykN%uU@lgbN|$b0313ypgBer;BD1&2V`;9)n|D#C5nq@qE>}iJToznBT3+Q1e|Lo7CEL*-+zm+BqG78X-;nFL?jFX8SA4zQlM6dCh?4N#}nVpAP_MY3=n1 zN&E<)o@kft5UG>&vmwe{OSo4l>TU@4G^T?ReTV8+z_{Fj0rgs_LO8aav}nhzv0Dxm z5p6QhUU(3B9+tbUBC05cSdNU^1)@0$>IL~Gd{1gkV$Ceba^4^SnA8em=1o+BeB5z% z*@?=oGNYuKkm*%vobQRYomURyP8l>LEM0(8x6^*bw>#{2M7`E1kBZ$smiW!^qaUb& zpx{777z>N7ws$Yl;Z+0hbDbpggeE6awkp{^gvfHTD#eEm{Al&h`q{_T)u0Jv&yB4&@Yu9Dt8p;KAGehfMz0%t1SdUK52Id-a@XqfpTTd#Z~3y ztS18Tk>bvADo*|SXZMi8p@lbt^!EigAQ(|1DCU2FA?GRSkOQno zQDFCUH^R@8l{E`1-cunUaasE z`%pENzjVLsnoXM$@PRS#h;aYe5I=Tptytz!YlL4N2OY;oS0+M-orRg)X7fC7MfQ#{ z+`%@&4lbbB{|FKZPXmvZ#{7FVt?~#*g2(6r6gqS+Pm$yT#icA{G}{QaCUl-<4Zdp+ ztzpjwukyZtK*9~ZvPOOoqXf_T3VQldOhayce?W|Q-thDefTca&Y_;j$*cS4=*FvkO zjg9#7Lt9|p6zG$zLnxW-l))P`K4lQKLqG>bq^07BocUV=XF~Dh{48ouEW%y1YXg&= zezP}0Qkr?jyW&eH#)fq&&{czoH6O}1=yvfLWTo5UBcykOyQJCAGI^86r(rO`i z>klE^<6DMSj8~0uj@AkTUJgL>=kw=#PN7W!rh=t`rkN*)={$z)7X%+Xw< z0mD}Djg)$9^+cX<(+vj(`+>VqE}Gqgk}p1RF_4_wynupt&zWr3MPvPm)KOoNqZy4n8ex|(4 ze-C)^(|DHXlEqFW%zLmx=x)8cCtN=}z@Ri@2uDEjx#f!lUL?d&*foVL8ifSc`D$*bF= zz#>8~H=rHgAubq(srCxN%gyVsLx0fgeY0ocv*g8tk?q;O>$fB2ZMTXfGR;ytLms26 zQf9Gi4!|3XwxT%G<>Aq9W(mV$9L}lJ(UVF|4S=-9EJL+2^ zPFX&~S5UWzd==pNQYI&^D@^Kg^iuJSX;b=JiXVrQhxX3ECMKQ!vQ!9mjX@^?($;v3 zuINq}H1jNR01u4^2SX09cSXK?oWIyF7!j9Es^Q?BB%HEm<%DoI_ie>--Gn8Do$)k$ zOFOw6Kh@hEm~m@%WN2NLDvRy&EbYwdkA2=S8Z?uH1J7|+A@A&dOm8oG0ya3=fwq_T z6dO?QILh6#ttTYm*v;h=jj|4#b~>jQg35muyBK?Ts9Kv`n%Q1#9UEBYY*-AyzMH*^ z^kREG#BxwJ=d=kWoVNE(aa!Q!QrS~}W9G^!vG%nD)2=Wyx=@>e`Lx13L~buL1BXMn zi9O!?KyM#)6Nwl41Qq#dW@@#7vH3rG8EpYA&FuzxNpCZrE>&N8EIh&nHoT(xw@;t5x`o$wi{42o@K5- zBCT3ql!ZI@dQVM*VQ4s-dqWM^4QY!|jtFko-Uuw?0ylxucDK|;cjj7cnt zlUzl5JhpNzGMvsZ&Tf4l8(g|7aTF)yKkK#auAc!~?LDr|MMJDrlylJO4Dd!r4WLrI z=0fbpU~(6~ST9(w*E17@ZWD$Z1>hAQRSlhF*S$=rP&W+ligXdf6j!x<7WJPoy}-LEjQ1B{E@Ro9uNHo&UkRcQ8K0slv#@vsi_h?szK=M; z!=mPbpm)Y@v--hdj_PQq?oloXAYAj=P(fso(;n3biidb==3C;%`(bIm8S;07yYP7Q zZ@=JQX_vOo3WcY3qKLAKnbrj(oZ)J5P51}P&4!)qnn4sYhV-HuRlS%SXr6?z53_{R zen9e&%`PcnmVhy?`Nh-4D>ub@bK=#nkD) zU88zIr#gUMp{C81(OOhp^RkhVKY=2_7~4-A_$8PFN=v$rK2#Br^d2|5HGy*AQ?^K)5{Uu*x?FU`o9&D(hJCl*UJ&tudWSmp;bP2OA5K}* z3}*=X4ymc?K*O5y08G`3SFoguuXdqK3LPMjHZ*b7_u4<9lZjzUqqeJ<%FLPUWjC(W~xNj75{Yz-lWFN(e%RPaA0?gE5>lx9#!(l8_ms; z)os|NlgsFSA^399#`so!Z*l5w@;ktvjRs2yH~o11C@r^9tx=Lc|D#faik#H20Ca9J zZM_sW`2k}OUy(~WsP~JeaV2u9)0?2d0AL0 zgoYi*-1DMY?yTFZ#gmAYOpou!6c5+_6z^7>@$(P4E<5uHMEmv^v1bx&JdCedK6&ZS zgFN;)?iNnmUkJlQaqp`CB&*46;ef#@7ATQaH{`cMnaPRhj|scSLQ2cZPfhFc&G_-n z2&J|1TS0ujvJuB~Mp1MmH1w~Bhy_h<8UXX62~rvS;vaKeh&-K?{@9km)H0B6wI6zh zy;V)PCop)O!-QL}JnOTv>W(#0_(yGBjvi_kNc5)6>}=D{zkgZ1I(QjkQH9&~wko=( zW6uGGgOOYh`Z?H7;Q= z@bftD+>2oh^KO;RjF9)Zx)fQ-{aSqOnu-IbudMcZKXv+Gh5=(hl{&elS~2;g%s6pd z`3Gl1G9R%B!g|%hzrsR!`RN(kXYCD?7P9u8@2A(G%)5_AtT@w+Fz>#M;sW;jzWAt+_qP3JS_k^ia1dxsCYm2;l*L8m{H90*xEa_ z0Wu_6WXqoWF6)}>T9g^9RAC+nBPcD!pF_eY3ir744iqh9m9Zx-e!1-5KQe2`OXS=T%Bc2NMP_hQ8-CgOL(=GwWw`c@06JWMNoA2;d$W6mOJ zll>)3!qA*-?}TC4ERoLW&(ive&wSAQj&QQJ#@sg%aG_k5mi60^3%j!m(l-HD?EOE^ zxpZ@D+fPXI`Q-|f#d#6cZ^{S->9;YT5*Gcu4sx#b80iLrg{16Vw8)^*67!%E)_0i{u0 zUn3ZKk;o3bwW!v=+m`Ov&LV^4FwK`^oLr9lkTsKcswEaf-s|vRD5}Ayw##mi^N_b#+q)T-^<*nRHc=) zvjkF@P-%;6p3E*$=|0_PVq?Cq|g3{mdseieC{Nurb z2w?qB;+cP3hWwZG+s@ySmLLEA^Y3r;ALPlWznqAEFY|AS|NWOo zsb9bTN9MooNRQwDDfiE(f7y+|8fud z!wi3T1(E77fjBrGBQSG=y>dY8;5Tm8SBU?MTM#G5{~m$)FTbn*xCL?k6Mz{J_~`oc zD*!Xt?dl(&peON{0j$4r2mKR(`4{r{rT*6?=vQu;|GETmast50{x6Rp5AEksYSYOA zJ1dLjdwYAwd&jQhV~Zy^j@QljW|^i2E(lrFF>62s2*?-gW&NrhCgDmJab&b**yT~L zhD;H(LO7HWcRCWv+pTQp;@Kdih3l&FV3`Khd6CXw!KD(#P+u*k~?A%{3z5RLq zbMt%R?HrhgA1F^OEo$hL^YM1k{{4CMoX6Oi8wD7UTD$t zNWl1A+`VpjdtkdU`iis>S}Xeu#Sy z7!U1vv0xW}xfnfiHZt54Jub0}W&XD8i{y3Gmh(;2y>H0Im+K!+xXJGNiiNni@Gls(*in-Rc6Dmals=p?*p(2`nnC)RNGl5d@GO=j zPSbWwOtlW}aUQ0qbUlR+1gDYcf?9Y{@1VR5g58^Z4&`T5)3lSy@5kG?`llkcg^JiT~qqx=*I-psxrV~iW+ynhOe4kO)4c5F7PWp|3_eeu74JFDkF0MU_ z0ntmZ;AU=Np#iZDIr(R@2FO}Qqo~Uh;RE&v19sH#17vt^*iEN)s%~FnK5~z%F)L6{ za!)>AlZkv5G$DYJCaM!Nx~x22Hgo({U=L#M##o4?;)Pb^n_2r7lsTC4PfOIf_@3@K z58%u^Gb@*(xZXq?f<(PPE;GLVY$y#J(r>l&#RX`dv5PWXn$yWz2giSomaNH0|B{eW z&r{>f3;zSXG+WZYrxS2iQKOC@gSqUr@0HzVdehHkWL2DEeaueMW#Mx{f0w7XtXwiF zwB50chYH8K(0+CeG4mN|qwW~i%|g1FYQ1%j4NqLNolnIIQxl>tT4g@1i+Pcv>eX?4 z{~Y!N|5atxs?yu@vD8b1e9}D0I3yzl9?@o#2U_9@3>@Nv5*k~JQJ4hCmUfEvNS}vx zz#67KcKx%Y#CQAseHQN_;D@xOM)&Z@kHg%P>O=CBzCdeGSD>qAC59Bab9EwVi@cXO2UbcKcz2! zv`(q{Vi}8L#NT6N==x<_?Z-+3?-@DA(ejy~7w>?7fsyi;oXmSG?j&1 zijl#eqxQCAdMXYml}O?3@0{7*Id$Vc+i=5U4i;@}ToTizUIb)neTXf3ex5~BZK#kDr zd%f2jyCwNy14^%)&hzJOfM#-X**_onvwOft1fgjf!gC@I(^ppaeO!!DZ{PiZidGUG z&i$x?6b*~p4-F(`E(s>E4(tj00c&0hFZ;ou7G5Ltk2ftNFl;Pqg8B2s-8ZIZM7?Af zTqck~a%)itgls;d)b{<*$jR;h0h18%$@DrT#q2h=RQ%Q0Q8X|_v$#HNc1(gj>cBZ# zE1lRq&Mk(aj^Sn?7QG9+q1wlMW_`RMm?8NP1%)?cg|v`S;L!AIN8}CiNMT``!fTH} z!n?~=#(fE;yv{cK9$SNZQ(iZI7xfMg+nR%3%6FQaMMnqmjvTgmOpdh#G_Kjvt)ktjm1 z1bL=e8K~%iBPT`_8a4OJu&b!=S4V}5vxyTJ7i1aU_ zf}BE)`mZmwU-O0OFEM;HPpi*qPryOrgd9-W%qIL|*b(d3YuPLq?!4(xK;X&@DIwM# z43CQjK|yXH1|1gcjfTcb{tNsLeS==X%CA_0>RRV&c8~aO|>>p!-*OJ-{+kVqoKo#!`?E59@R$}t%HFfNV zY8z2X6ryvB&+7|=n_A(?eHk$5vVpFp@mlpxEu@8bOng4d(qZ^M=I4|Hre?#SCL7HV zZTU|X2QR3Tl*#Pbe5yOgW0MWoFe(yxLb?m$>qUmg6>1ol#_bBXUA>^2DtSH>DdUs$ zwBU|IR)_;?7wkvaCAkpewv4p|#fdP&>@=X5%s%dtz&TShMbAF>z_E5vjey-0TiGr; zkqh!0%vgw(KUXQMo^j+!E^{q+c9sOqXXzpZOr|^O=?LJxAWV;`9=Gn?wkM^OY%xx4 zIa8uUjr`tyvf{}QQsnR68v#px_AGM9yjhtW6aGBKn8hk0QHq}~tdmsgRhp}f@T4j0 z&j%Gx!qqpDU#)zIkfWSRST%{z>`dM^eu!VF-1GHcg6zECSg{{3zW*$ke4!f3iF-xd zhG*|CsvLRDt*Ca@=*L(4 z#L;k(FKoc6LX#5kbzq}4>M&>8$Am3&=@eG9bwfq&%4Mr?X9iXgxOsU4cr}(TC>`Xa zhc3}-MG~-h39qLy=sIr1fj3mw#g#2RF6X}}&Z^Z(ghHc=*tkGi>$67}7F!8aH6%fPoS#MjRMi+3TAE#-Tp@&tnlwbDC2N8a35ijA^UvD)?TfN zwl}}`pUWJ_6v)A8>_vm0<1w>u%t@d$kZkct+t_*t;5kT1T~I0x@O3+LP4u`O_~F$m zYWuVC*h2E)VI*XZJHy?9nFuKiskPC4ZCd|m{_exjyVXvrFW-*aRQr5gFWz{O6FN7x z3Qz_qFSKy5-rTP6(LWah#;@cmIn>#YPa!}V&%i(3@ z`S3!C9u9X|TV#97JJ>-;jGHu@5LFpDm=;bf4vKsb0_{I=W;{zq<{4G{(g3Ex_nw}; zEIj?dv#pr3%Fj7d;DmmQJ_SC0?Qfp#k@O}SzQ^l@`|8n8W5@yNS}RpQfDqXp%m)LJ zRiLn8E$ETN)6(kVFqpD>>6QCl!7sy8=zz^*X3;u;e+EPF{WnYx%hfUBP`Bp!Tixdz zx{V^%CsC@ZIZbu^*BI0JTtw2#jq&gkxZ0e)f)K<-07kKJwHb%vTDWpRQ=#37!f z_AC`ue}1_oDr7#5k@I89xqx|U=ny#cl8D(;8_wqvL{@qe`!QzP8<;3gS@fW4)JVtC zFr=wbEy_5&CcH?RSm3n+IacIU@FGjPr@d9axg|M|S#%eI7Z)KSA}>XjQsF2B$+e+v zWGIa^+!1aMa~X|l)&i|XmiaVhw!nzyK(;46VWD5lpWeDjP*( zQ!zs6lpq&EM$w@75!Hz#l#6E`?aphKYozvsxR5I_0?=B)v!qD9->Y!FUTHZHp+a^( zW7wjmI6Ti|#hFX?CsC1^D8r&9`ZBuF!ifFF@xfeD6P(UOd+&R`2-HhkFP@IRg*Zf zw<6K?INSz!4(-@y>Zi3IJ#|W@x?j9F!%KP>DoODIfD{fRgtiMu4C4$H`L(j(7%mI~h)c%(unvo(h)WE1(rlPjWL9syh0-Z8zc@sW zyWb4Yiw2F}zr1O1_&!xUXo}zJ7B5lP{C2H9nH>(fRX*{RQkm*v!1FJkbb}P)*4!&< zAkGrW1&w-=lg+WQ)#p|X2_$%Etk5}J6WHGI=eT=_``;wD>yzQG63Rj-L#uYiJ=J=fEF9b-%;U>JH6ka2c*zYn5g@aPKYzkF0BxT(x{X>mJar=lQi%%{IJzI z%WBqb6R5pOd~olJ4Hra1Y)E5yy~yMB(qthFX|eK{clKB}dUS)qigy_(#uKHCE(E8c zgG5w}r&BBi2^S`|M;ZG}2gAS`(nj996rq9xqK>+K26`^#7x+=Wy&AzYM%tj4QGxBS z>tjm#C$cw%G%Rp)2hZGz49cMnoxEF+D`p|Cc-r-Tfn2WK?PQXBn_EJjj;FD+l*7dz zeU`Q~;7`m>@P)C}bjBfGr|XUSEJ*Nv{5Co4wTJlAQvHX^n^<#-Z;V})JyP9+M15Cg z6l3qgSr;kQj^_ezTF?#!^OPA?#l30Ahd((*XS_Gg-Zv_>=^;+Zw&QS}Zkix2r#UN? za~IQUsHP`I>9ZwCjfvKZOIgfza=zOXx?^!p*{HPdyqXi_$lW&SVyam5%ZX!{u)F0Jn*41Fe*3Jj--Glr&UWqo9^NU;|+;bvbX(WhgfjvT}A)$TAxiL zT*g#KIOMpgs%ph^+sJ)Ul=r(nDEy~cZ~_X`&~dp{(u_OpM3?ReVlORlVJ!JZ#^ zqoq?$x2e^>R1Z$_<-Q#Zn`ku(W2f%8i7GT0cIl?jsTj*RvJq=)l>XEQUB{9z`HVM0 z!xLEp&!BnR?ft7X8AJbDDOZB93I3J* zq6Uqv6qc2lueq-V7deh%Q+G0~hqiRWaK;+)>r595HzPT=_yrH>zZxvzfZYXarXw+2;eAmAqC_i|hDi|U+En!-);w7mCEK8boDswtHR}WAk~?NC z`9W;X7Wh5t-8;-0OQ(vnS_F5xeKSK>?=)fcOSLblvZQ8b2*f79p3tw#%hKfu%tkzW zSH^D^QsxH>s+&I6f`EwE3LXY~kZmYlCrG4xm2l@7-}rF9CweNtSC*2SYDGz<9e#o7 z8|&@`WiXPjLf^H?Cq$n_soxuZ@<=n5RFBg>-)1?|TS`x9`TBL_!J5-JJx?D6-}>3r z&zq0W^qxgP()j2@wZRBL+L1!YdSk#tF$v+EG8rIMV7aVETy?pq*s1z=EkE4&3;Zn6 z2MBO~^>h{y{!L#PF@YsD-JaqrR$zsi`Bg zwW%xJKazjvSfd4RhmtC)l4^nX0cNdD?2L(n3?(bBDMR4v9hsqfWU3|4Fj=r za)TBAhJnAZfhmT+Vc@bruqB>gAg-tNv9tcgJN>sD0LTpjb60;W3*h1ca6Hiq-~a-^ zEYaWP*ja&(Jkx*I2Tpaz3Z_f`h5@)afsc&U-{jcXxxsYJ-!K3tknK-7PL9WX?Efwc z0CKPbpR^flejs?e^f$S`zC6*v3Icc4)3P9TZXkG%_TM_d_TUCHNq@&c9Bfbfg%tz@ zU)@tVFh}&yz6W>A6FD~EU*yo=^>T7>{vpQ+0&{GCZwuI#z^B*C4tV6?{w~J>0v`fa|Fau+<(9Sboz10%r$(+Akd7 z2L6G8NxDzGw8+@6@OF8+f4rPk~S5z-tBQ$=C(2A6(#ksK3_-o?o8Y=y5K2jFa&A z`rAfe4D@u2gE8=e`KKKC81HFW?x(hVTxXuHp;gnHbJL diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx b/doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx deleted file mode 100644 index f8b98a6f1f8e4afab4de9892cd8cd8920c6538f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105265 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 z 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 F{U7 Date: Mon, 1 Oct 2018 23:52:55 +1200 Subject: [PATCH 75/87] STYLE: Added explicit exceptions (#22907) --- pandas/compat/pickle_compat.py | 8 ++++---- pandas/tseries/holiday.py | 6 +++--- pandas/util/_print_versions.py | 6 +++--- pandas/util/_validators.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pandas/compat/pickle_compat.py b/pandas/compat/pickle_compat.py index c1a9a9fc1ed13..713a5b1120beb 100644 --- a/pandas/compat/pickle_compat.py +++ b/pandas/compat/pickle_compat.py @@ -33,7 +33,7 @@ def load_reduce(self): cls = args[0] stack[-1] = object.__new__(cls) return - except: + except TypeError: pass # try to re-encode the arguments @@ -44,7 +44,7 @@ def load_reduce(self): try: stack[-1] = func(*args) return - except: + except TypeError: pass # unknown exception, re-raise @@ -182,7 +182,7 @@ def load_newobj_ex(self): try: Unpickler.dispatch[pkl.NEWOBJ_EX[0]] = load_newobj_ex -except: +except (AttributeError, KeyError): pass @@ -210,5 +210,5 @@ def load(fh, encoding=None, compat=False, is_verbose=False): up.is_verbose = is_verbose return up.load() - except: + except (ValueError, TypeError): raise diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py index b9c89c4e314f9..0497a827e2e1b 100644 --- a/pandas/tseries/holiday.py +++ b/pandas/tseries/holiday.py @@ -294,7 +294,7 @@ def _apply_rule(self, dates): def register(cls): try: name = cls.name - except: + except AttributeError: name = cls.__name__ holiday_calendars[name] = cls @@ -426,7 +426,7 @@ def merge_class(base, other): """ try: other = other.rules - except: + except AttributeError: pass if not isinstance(other, list): @@ -435,7 +435,7 @@ def merge_class(base, other): try: base = base.rules - except: + except AttributeError: pass if not isinstance(base, list): diff --git a/pandas/util/_print_versions.py b/pandas/util/_print_versions.py index 5600834f3b615..03fc82a3acef5 100644 --- a/pandas/util/_print_versions.py +++ b/pandas/util/_print_versions.py @@ -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: @@ -50,7 +50,7 @@ def get_sys_info(): ("LANG", "{lang}".format(lang=os.environ.get('LANG', "None"))), ("LOCALE", '.'.join(map(str, locale.getlocale()))), ]) - except: + except (KeyError, ValueError): pass return blob @@ -108,7 +108,7 @@ 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): diff --git a/pandas/util/_validators.py b/pandas/util/_validators.py index a96563051e7de..e51e0c88e5b95 100644 --- a/pandas/util/_validators.py +++ b/pandas/util/_validators.py @@ -59,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: From b92b0431bee1a3faa743aefbf72ec17ae1f80518 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 1 Oct 2018 08:08:59 -0400 Subject: [PATCH 76/87] Loc enhancements (#22826) --- asv_bench/benchmarks/indexing.py | 75 +++++++++++++++++++------------- doc/source/whatsnew/v0.24.0.txt | 2 + pandas/_libs/index.pyx | 25 +++++++---- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/asv_bench/benchmarks/indexing.py b/asv_bench/benchmarks/indexing.py index 739ad6a3d278b..c5b147b152aa6 100644 --- a/asv_bench/benchmarks/indexing.py +++ b/asv_bench/benchmarks/indexing.py @@ -11,95 +11,110 @@ class NumericSeriesIndexing(object): goal_time = 0.2 - params = [Int64Index, Float64Index] - param = ['index'] + params = [ + (Int64Index, Float64Index), + ('unique_monotonic_inc', 'nonunique_monotonic_inc'), + ] + param_names = ['index_dtype', 'index_structure'] - def setup(self, index): + def setup(self, index, index_structure): N = 10**6 - idx = index(range(N)) - self.data = Series(np.random.rand(N), index=idx) + 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_scalar(self, index): + def time_getitem_scalar(self, index, index_structure): self.data[800000] - def time_getitem_slice(self, index): + def time_getitem_slice(self, index, index_structure): self.data[:800000] - def time_getitem_list_like(self, index): + def time_getitem_list_like(self, index, index_structure): self.data[[800000]] - def time_getitem_array(self, index): + def time_getitem_array(self, index, index_structure): self.data[self.array] - def time_getitem_lists(self, index): + def time_getitem_lists(self, index, index_structure): self.data[self.array_list] - def time_iloc_array(self, index): + def time_iloc_array(self, index, index_structure): self.data.iloc[self.array] - def time_iloc_list_like(self, index): + def time_iloc_list_like(self, index, index_structure): self.data.iloc[[800000]] - def time_iloc_scalar(self, index): + def time_iloc_scalar(self, index, index_structure): self.data.iloc[800000] - def time_iloc_slice(self, index): + def time_iloc_slice(self, index, index_structure): self.data.iloc[:800000] - def time_ix_array(self, index): + def time_ix_array(self, index, index_structure): self.data.ix[self.array] - def time_ix_list_like(self, index): + def time_ix_list_like(self, index, index_structure): self.data.ix[[800000]] - def time_ix_scalar(self, index): + def time_ix_scalar(self, index, index_structure): self.data.ix[800000] - def time_ix_slice(self, index): + def time_ix_slice(self, index, index_structure): self.data.ix[:800000] - def time_loc_array(self, index): + def time_loc_array(self, index, index_structure): self.data.loc[self.array] - def time_loc_list_like(self, index): + def time_loc_list_like(self, index, index_structure): self.data.loc[[800000]] - def time_loc_scalar(self, index): + def time_loc_scalar(self, index, index_structure): self.data.loc[800000] - def time_loc_slice(self, index): + def time_loc_slice(self, index, index_structure): self.data.loc[:800000] class NonNumericSeriesIndexing(object): goal_time = 0.2 - params = ['string', 'datetime'] - param_names = ['index'] + params = [ + ('string', 'datetime'), + ('unique_monotonic_inc', 'nonunique_monotonic_inc'), + ] + param_names = ['index_dtype', 'index_structure'] - def setup(self, index): - N = 10**5 + 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 time_getitem_label_slice(self, index): + def time_getitem_label_slice(self, index, index_structure): self.s[:self.lbl] - def time_getitem_pos_slice(self, index): + def time_getitem_pos_slice(self, index, index_structure): self.s[:80000] - def time_get_value(self, index): + def time_get_value(self, index, index_structure): with warnings.catch_warnings(record=True): self.s.get_value(self.lbl) - def time_getitem_scalar(self, index): + def time_getitem_scalar(self, index, index_structure): self.s[self.lbl] + def time_getitem_list_like(self, index, index_structure): + self.s[[self.lbl]] + class DataFrameStringIndexing(object): diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3e1711edb0f27..6bb1ddfe2324d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -610,6 +610,8 @@ Performance Improvements :meth:`~HDFStore.keys`. (i.e. ``x in store`` checks are much faster) (:issue:`21372`) - Improved the performance of :func:`pandas.get_dummies` with ``sparse=True`` (:issue:`21997`) +- Improved performance of :func:`IndexEngine.get_indexer_non_unique` for sorted, non-unique indexes (:issue:`9466`) + .. _whatsnew_0240.docs: diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 562c1ba218141..3f76915655f58 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -294,14 +294,23 @@ cdef class IndexEngine: result = np.empty(n_alloc, dtype=np.int64) missing = np.empty(n_t, dtype=np.int64) - # form the set of the results (like ismember) - members = np.empty(n, dtype=np.uint8) - for i in range(n): - val = values[i] - if val in stargets: - if val not in d: - d[val] = [] - d[val].append(i) + # map each starget to its position in the index + if stargets and len(stargets) < 5 and self.is_monotonic_increasing: + # if there are few enough stargets and the index is monotonically + # increasing, then use binary search for each starget + for starget in stargets: + start = values.searchsorted(starget, side='left') + end = values.searchsorted(starget, side='right') + if start != end: + d[starget] = list(range(start, end)) + else: + # otherwise, map by iterating through all items in the index + for i in range(n): + val = values[i] + if val in stargets: + if val not in d: + d[val] = [] + d[val].append(i) for i in range(n_t): val = targets[i] From f021bbccd0d864b7276dc1a9c915b5ad45b5f6fd Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Mon, 1 Oct 2018 14:10:49 +0200 Subject: [PATCH 77/87] Fix Timestamp.round errors (#22802) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/_libs/tslibs/timestamps.pyx | 137 +++++++++++++----- pandas/core/indexes/datetimelike.py | 12 +- .../indexes/datetimes/test_scalar_compat.py | 43 +++++- .../tests/scalar/timestamp/test_unary_ops.py | 43 +++++- 5 files changed, 192 insertions(+), 44 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 6bb1ddfe2324d..5532771b38a0e 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -654,6 +654,7 @@ Datetimelike - Bug in :class:`DatetimeIndex` subtraction that incorrectly failed to raise ``OverflowError`` (:issue:`22492`, :issue:`22508`) - Bug in :class:`DatetimeIndex` incorrectly allowing indexing with ``Timedelta`` object (:issue:`20464`) - Bug in :class:`DatetimeIndex` where frequency was being set if original frequency was ``None`` (:issue:`22150`) +- Bug in rounding methods of :class:`DatetimeIndex` (:meth:`~DatetimeIndex.round`, :meth:`~DatetimeIndex.ceil`, :meth:`~DatetimeIndex.floor`) and :class:`Timestamp` (:meth:`~Timestamp.round`, :meth:`~Timestamp.ceil`, :meth:`~Timestamp.floor`) could give rise to loss of precision (:issue:`22591`) Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index e985a519c3046..0c2753dbc6f28 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -22,6 +22,7 @@ cimport ccalendar from conversion import tz_localize_to_utc, normalize_i8_timestamps from conversion cimport (tz_convert_single, _TSObject, convert_to_tsobject, convert_datetime_to_tsobject) +import enum from fields import get_start_end_field, get_date_name_field from nattype import NaT from nattype cimport NPY_NAT @@ -57,50 +58,114 @@ cdef inline object create_timestamp_from_ts(int64_t value, return ts_base -def round_ns(values, rounder, freq): +@enum.unique +class RoundTo(enum.Enum): """ - Applies rounding function at given frequency + enumeration defining the available rounding modes + + Attributes + ---------- + MINUS_INFTY + round towards -∞, or floor [2]_ + PLUS_INFTY + round towards +∞, or ceil [3]_ + NEAREST_HALF_EVEN + round to nearest, tie-break half to even [6]_ + NEAREST_HALF_MINUS_INFTY + round to nearest, tie-break half to -∞ [5]_ + NEAREST_HALF_PLUS_INFTY + round to nearest, tie-break half to +∞ [4]_ + + + References + ---------- + .. [1] "Rounding - Wikipedia" + https://en.wikipedia.org/wiki/Rounding + .. [2] "Rounding down" + https://en.wikipedia.org/wiki/Rounding#Rounding_down + .. [3] "Rounding up" + https://en.wikipedia.org/wiki/Rounding#Rounding_up + .. [4] "Round half up" + https://en.wikipedia.org/wiki/Rounding#Round_half_up + .. [5] "Round half down" + https://en.wikipedia.org/wiki/Rounding#Round_half_down + .. [6] "Round half to even" + https://en.wikipedia.org/wiki/Rounding#Round_half_to_even + """ + MINUS_INFTY = 0 + PLUS_INFTY = 1 + NEAREST_HALF_EVEN = 2 + NEAREST_HALF_PLUS_INFTY = 3 + NEAREST_HALF_MINUS_INFTY = 4 + + +cdef inline _npdivmod(x1, x2): + """implement divmod for numpy < 1.13""" + return np.floor_divide(x1, x2), np.remainder(x1, x2) + + +try: + from numpy import divmod as npdivmod +except ImportError: + npdivmod = _npdivmod + + +cdef inline _floor_int64(values, unit): + return values - np.remainder(values, unit) + +cdef inline _ceil_int64(values, unit): + return values + np.remainder(-values, unit) + +cdef inline _rounddown_int64(values, unit): + return _ceil_int64(values - unit//2, unit) + +cdef inline _roundup_int64(values, unit): + return _floor_int64(values + unit//2, unit) + + +def round_nsint64(values, mode, freq): + """ + Applies rounding mode at given frequency Parameters ---------- values : :obj:`ndarray` - rounder : function, eg. 'ceil', 'floor', 'round' + mode : instance of `RoundTo` enumeration freq : str, obj Returns ------- :obj:`ndarray` """ + + if not isinstance(mode, RoundTo): + raise ValueError('mode should be a RoundTo member') + unit = to_offset(freq).nanos - # GH21262 If the Timestamp is multiple of the freq str - # don't apply any rounding - mask = values % unit == 0 - if mask.all(): - return values - r = values.copy() - - if unit < 1000: - # for nano rounding, work with the last 6 digits separately - # due to float precision - buff = 1000000 - r[~mask] = (buff * (values[~mask] // buff) + - unit * (rounder((values[~mask] % buff) * - (1 / float(unit)))).astype('i8')) - else: - if unit % 1000 != 0: - msg = 'Precision will be lost using frequency: {}' - warnings.warn(msg.format(freq)) - # GH19206 - # to deal with round-off when unit is large - if unit >= 1e9: - divisor = 10 ** int(np.log10(unit / 1e7)) - else: - divisor = 10 - r[~mask] = (unit * rounder((values[~mask] * - (divisor / float(unit))) / divisor) - .astype('i8')) - return r + if mode is RoundTo.MINUS_INFTY: + return _floor_int64(values, unit) + elif mode is RoundTo.PLUS_INFTY: + return _ceil_int64(values, unit) + elif mode is RoundTo.NEAREST_HALF_MINUS_INFTY: + return _rounddown_int64(values, unit) + elif mode is RoundTo.NEAREST_HALF_PLUS_INFTY: + return _roundup_int64(values, unit) + elif mode is RoundTo.NEAREST_HALF_EVEN: + # for odd unit there is no need of a tie break + if unit % 2: + return _rounddown_int64(values, unit) + quotient, remainder = npdivmod(values, unit) + mask = np.logical_or( + remainder > (unit // 2), + np.logical_and(remainder == (unit // 2), quotient % 2) + ) + quotient[mask] += 1 + return quotient * unit + + # if/elif above should catch all rounding modes defined in enum 'RoundTo': + # if flow of control arrives here, it is a bug + assert False, "round_nsint64 called with an unrecognized rounding mode" # This is PITA. Because we inherit from datetime, which has very specific @@ -656,7 +721,7 @@ class Timestamp(_Timestamp): return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq) - def _round(self, freq, rounder, ambiguous='raise'): + def _round(self, freq, mode, ambiguous='raise'): if self.tz is not None: value = self.tz_localize(None).value else: @@ -665,7 +730,7 @@ class Timestamp(_Timestamp): value = np.array([value], dtype=np.int64) # Will only ever contain 1 element for timestamp - r = round_ns(value, rounder, freq)[0] + r = round_nsint64(value, mode, freq)[0] result = Timestamp(r, unit='ns') if self.tz is not None: result = result.tz_localize(self.tz, ambiguous=ambiguous) @@ -694,7 +759,7 @@ class Timestamp(_Timestamp): ------ ValueError if the freq cannot be converted """ - return self._round(freq, np.round, ambiguous) + return self._round(freq, RoundTo.NEAREST_HALF_EVEN, ambiguous) def floor(self, freq, ambiguous='raise'): """ @@ -715,7 +780,7 @@ class Timestamp(_Timestamp): ------ ValueError if the freq cannot be converted """ - return self._round(freq, np.floor, ambiguous) + return self._round(freq, RoundTo.MINUS_INFTY, ambiguous) def ceil(self, freq, ambiguous='raise'): """ @@ -736,7 +801,7 @@ class Timestamp(_Timestamp): ------ ValueError if the freq cannot be converted """ - return self._round(freq, np.ceil, ambiguous) + return self._round(freq, RoundTo.PLUS_INFTY, ambiguous) @property def tz(self): diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 578167a7db500..f7f4f187f6202 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -11,7 +11,7 @@ import numpy as np from pandas._libs import lib, iNaT, NaT -from pandas._libs.tslibs.timestamps import round_ns +from pandas._libs.tslibs.timestamps import round_nsint64, RoundTo from pandas.core.dtypes.common import ( ensure_int64, @@ -180,10 +180,10 @@ class TimelikeOps(object): """ ) - def _round(self, freq, rounder, ambiguous): + def _round(self, freq, mode, ambiguous): # round the local times values = _ensure_datetimelike_to_i8(self) - result = round_ns(values, rounder, freq) + result = round_nsint64(values, mode, freq) result = self._maybe_mask_results(result, fill_value=NaT) attribs = self._get_attributes_dict() @@ -197,15 +197,15 @@ def _round(self, freq, rounder, ambiguous): @Appender((_round_doc + _round_example).format(op="round")) def round(self, freq, ambiguous='raise'): - return self._round(freq, np.round, ambiguous) + return self._round(freq, RoundTo.NEAREST_HALF_EVEN, ambiguous) @Appender((_round_doc + _floor_example).format(op="floor")) def floor(self, freq, ambiguous='raise'): - return self._round(freq, np.floor, ambiguous) + return self._round(freq, RoundTo.MINUS_INFTY, ambiguous) @Appender((_round_doc + _ceil_example).format(op="ceil")) def ceil(self, freq, ambiguous='raise'): - return self._round(freq, np.ceil, ambiguous) + return self._round(freq, RoundTo.PLUS_INFTY, ambiguous) class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin): diff --git a/pandas/tests/indexes/datetimes/test_scalar_compat.py b/pandas/tests/indexes/datetimes/test_scalar_compat.py index 6f6f4eb8d24e3..d054121c6dfab 100644 --- a/pandas/tests/indexes/datetimes/test_scalar_compat.py +++ b/pandas/tests/indexes/datetimes/test_scalar_compat.py @@ -11,6 +11,7 @@ import pandas as pd from pandas import date_range, Timestamp, DatetimeIndex +from pandas.tseries.frequencies import to_offset class TestDatetimeIndexOps(object): @@ -124,7 +125,7 @@ def test_round(self, tz_naive_fixture): expected = DatetimeIndex(['2016-10-17 12:00:00.001501030']) tm.assert_index_equal(result, expected) - with tm.assert_produces_warning(): + with tm.assert_produces_warning(False): ts = '2016-10-17 12:00:00.001501031' DatetimeIndex([ts]).round('1010ns') @@ -169,6 +170,46 @@ def test_ceil_floor_edge(self, test_input, rounder, freq, expected): expected = DatetimeIndex(list(expected)) assert expected.equals(result) + @pytest.mark.parametrize('start, index_freq, periods', [ + ('2018-01-01', '12H', 25), + ('2018-01-01 0:0:0.124999', '1ns', 1000), + ]) + @pytest.mark.parametrize('round_freq', [ + '2ns', '3ns', '4ns', '5ns', '6ns', '7ns', + '250ns', '500ns', '750ns', + '1us', '19us', '250us', '500us', '750us', + '1s', '2s', '3s', + '12H', '1D', + ]) + def test_round_int64(self, start, index_freq, periods, round_freq): + dt = DatetimeIndex(start=start, freq=index_freq, periods=periods) + unit = to_offset(round_freq).nanos + + # test floor + result = dt.floor(round_freq) + diff = dt.asi8 - result.asi8 + mod = result.asi8 % unit + assert (mod == 0).all(), "floor not a {} multiple".format(round_freq) + assert (0 <= diff).all() and (diff < unit).all(), "floor error" + + # test ceil + result = dt.ceil(round_freq) + diff = result.asi8 - dt.asi8 + mod = result.asi8 % unit + assert (mod == 0).all(), "ceil not a {} multiple".format(round_freq) + assert (0 <= diff).all() and (diff < unit).all(), "ceil error" + + # test round + result = dt.round(round_freq) + diff = abs(result.asi8 - dt.asi8) + mod = result.asi8 % unit + assert (mod == 0).all(), "round not a {} multiple".format(round_freq) + assert (diff <= unit // 2).all(), "round error" + if unit % 2 == 0: + assert ( + result.asi8[diff == unit // 2] % 2 == 0 + ).all(), "round half to even error" + # ---------------------------------------------------------------- # DatetimeIndex.normalize diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index f83aa31edf95a..b6c783dc07aec 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -13,6 +13,7 @@ from pandas._libs.tslibs import conversion from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG from pandas import Timestamp, NaT +from pandas.tseries.frequencies import to_offset class TestTimestampUnaryOps(object): @@ -70,7 +71,7 @@ def test_round_subsecond(self): assert result == expected def test_round_nonstandard_freq(self): - with tm.assert_produces_warning(): + with tm.assert_produces_warning(False): Timestamp('2016-10-17 12:00:00.001501031').round('1010ns') def test_round_invalid_arg(self): @@ -154,6 +155,46 @@ def test_round_dst_border(self, method): with pytest.raises(pytz.AmbiguousTimeError): getattr(ts, method)('H', ambiguous='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 From a277e4aec312abe78689ce361025cd60b25cf0c0 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Mon, 1 Oct 2018 05:12:35 -0700 Subject: [PATCH 78/87] BUG: Merge timezone aware data with DST (#22825) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/indexes/datetimelike.py | 59 ++++++++++++++++-------- pandas/tests/indexing/test_coercion.py | 40 +++++++++++----- pandas/tests/reshape/merge/test_merge.py | 24 ++++++++++ 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 5532771b38a0e..b71edcf1f6f51 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -815,6 +815,7 @@ Reshaping - Bug in :meth:`Series.replace` and meth:`DataFrame.replace` when dict is used as the ``to_replace`` value and one key in the dict is is another key's value, the results were inconsistent between using integer key and using string key (:issue:`20656`) - Bug in :meth:`DataFrame.drop_duplicates` for empty ``DataFrame`` which incorrectly raises an error (:issue:`20516`) - Bug in :func:`pandas.wide_to_long` when a string is passed to the stubnames argument and a column name is a substring of that stubname (:issue:`22468`) +- Bug in :func:`merge` when merging ``datetime64[ns, tz]`` data that contained a DST transition (:issue:`18885`) Build Changes ^^^^^^^^^^^^^ diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index f7f4f187f6202..37a12a588db03 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -277,7 +277,7 @@ def _evaluate_compare(self, other, op): except TypeError: return result - def _ensure_localized(self, result, ambiguous='raise'): + def _ensure_localized(self, arg, ambiguous='raise', from_utc=False): """ ensure that we are re-localized @@ -286,9 +286,11 @@ def _ensure_localized(self, result, ambiguous='raise'): Parameters ---------- - result : DatetimeIndex / i8 ndarray - ambiguous : str, bool, or bool-ndarray - default 'raise' + arg : DatetimeIndex / i8 ndarray + ambiguous : str, bool, or bool-ndarray, default 'raise' + from_utc : bool, default False + If True, localize the i8 ndarray to UTC first before converting to + the appropriate tz. If False, localize directly to the tz. Returns ------- @@ -297,10 +299,13 @@ def _ensure_localized(self, result, ambiguous='raise'): # 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, ambiguous=ambiguous) - return result + if not isinstance(arg, ABCIndexClass): + arg = self._simple_new(arg) + if from_utc: + arg = arg.tz_localize('UTC').tz_convert(self.tz) + else: + arg = arg.tz_localize(self.tz, ambiguous=ambiguous) + return arg def _box_values_as_index(self): """ @@ -622,11 +627,11 @@ def repeat(self, repeats, *args, **kwargs): @Appender(_index_shared_docs['where'] % _index_doc_kwargs) def where(self, cond, other=None): - other = _ensure_datetimelike_to_i8(other) - values = _ensure_datetimelike_to_i8(self) + other = _ensure_datetimelike_to_i8(other, to_utc=True) + values = _ensure_datetimelike_to_i8(self, to_utc=True) result = np.where(cond, values, other).astype('i8') - result = self._ensure_localized(result) + result = self._ensure_localized(result, from_utc=True) return self._shallow_copy(result, **self._get_attributes_dict()) @@ -695,23 +700,37 @@ def astype(self, dtype, copy=True): return super(DatetimeIndexOpsMixin, self).astype(dtype, copy=copy) -def _ensure_datetimelike_to_i8(other): - """ helper for coercing an input scalar or array to i8 """ +def _ensure_datetimelike_to_i8(other, to_utc=False): + """ + helper for coercing an input scalar or array to i8 + + Parameters + ---------- + other : 1d array + to_utc : bool, default False + If True, convert the values to UTC before extracting the i8 values + If False, extract the i8 values directly. + + Returns + ------- + i8 1d array + """ if is_scalar(other) and isna(other): - other = iNaT + return 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 + if to_utc: + other = other.tz_convert('UTC') + else: + other = other.tz_localize(None) else: try: - other = np.array(other, copy=False).view('i8') + return np.array(other, copy=False).view('i8') except TypeError: # period array cannot be coerces to int - other = Index(other).asi8 - return other + other = Index(other) + return other.asi8 def wrap_arithmetic_op(self, other, result): diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index e7daefffe5f6f..2f44cb36eeb11 100644 --- a/pandas/tests/indexing/test_coercion.py +++ b/pandas/tests/indexing/test_coercion.py @@ -590,11 +590,9 @@ def test_where_series_datetime64(self, fill_val, exp_dtype): pd.Timestamp('2011-01-03'), values[3]]) self._assert_where_conversion(obj, cond, values, exp, exp_dtype) - @pytest.mark.parametrize("fill_val,exp_dtype", [ - (pd.Timestamp('2012-01-01'), 'datetime64[ns]'), - (pd.Timestamp('2012-01-01', tz='US/Eastern'), np.object)], - ids=['datetime64', 'datetime64tz']) - def test_where_index_datetime(self, fill_val, exp_dtype): + def test_where_index_datetime(self): + fill_val = pd.Timestamp('2012-01-01') + exp_dtype = 'datetime64[ns]' obj = pd.Index([pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), pd.Timestamp('2011-01-03'), @@ -613,13 +611,33 @@ def test_where_index_datetime(self, fill_val, exp_dtype): pd.Timestamp('2011-01-03'), pd.Timestamp('2012-01-04')]) - if fill_val.tz: - self._assert_where_conversion(obj, cond, values, exp, - 'datetime64[ns]') - pytest.xfail("ToDo: do not ignore timezone, must be object") self._assert_where_conversion(obj, cond, values, exp, exp_dtype) - pytest.xfail("datetime64 + datetime64 -> datetime64 must support" - " scalar") + + @pytest.mark.xfail( + reason="GH 22839: do not ignore timezone, must be object") + def test_where_index_datetimetz(self): + fill_val = pd.Timestamp('2012-01-01', tz='US/Eastern') + exp_dtype = np.object + obj = pd.Index([pd.Timestamp('2011-01-01'), + pd.Timestamp('2011-01-02'), + pd.Timestamp('2011-01-03'), + pd.Timestamp('2011-01-04')]) + assert obj.dtype == 'datetime64[ns]' + cond = pd.Index([True, False, True, False]) + + msg = ("Index\\(\\.\\.\\.\\) must be called with a collection " + "of some kind") + with tm.assert_raises_regex(TypeError, msg): + obj.where(cond, fill_val) + + values = pd.Index(pd.date_range(fill_val, periods=4)) + exp = pd.Index([pd.Timestamp('2011-01-01'), + pd.Timestamp('2012-01-02', tz='US/Eastern'), + pd.Timestamp('2011-01-03'), + pd.Timestamp('2012-01-04', tz='US/Eastern')], + dtype=exp_dtype) + + self._assert_where_conversion(obj, cond, values, exp, exp_dtype) def test_where_index_complex128(self): pass diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py index 42df4511578f1..50ef622a4147f 100644 --- a/pandas/tests/reshape/merge/test_merge.py +++ b/pandas/tests/reshape/merge/test_merge.py @@ -601,6 +601,30 @@ def test_merge_on_datetime64tz(self): assert result['value_x'].dtype == 'datetime64[ns, US/Eastern]' assert result['value_y'].dtype == 'datetime64[ns, US/Eastern]' + 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') From 5ce06b5bdb8c44043c6463bf8ce3da758800a189 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Mon, 1 Oct 2018 14:22:20 -0700 Subject: [PATCH 79/87] BUG: to_datetime preserves name of Index argument in the result (#22918) * BUG: to_datetime preserves name of Index argument in the result * correct test --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/tools/datetimes.py | 13 ++++++++----- pandas/tests/indexes/datetimes/test_tools.py | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index b71edcf1f6f51..851c1a3fbd6e9 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -655,6 +655,7 @@ Datetimelike - Bug in :class:`DatetimeIndex` incorrectly allowing indexing with ``Timedelta`` object (:issue:`20464`) - Bug in :class:`DatetimeIndex` where frequency was being set if original frequency was ``None`` (:issue:`22150`) - Bug in rounding methods of :class:`DatetimeIndex` (:meth:`~DatetimeIndex.round`, :meth:`~DatetimeIndex.ceil`, :meth:`~DatetimeIndex.floor`) and :class:`Timestamp` (:meth:`~Timestamp.round`, :meth:`~Timestamp.ceil`, :meth:`~Timestamp.floor`) could give rise to loss of precision (:issue:`22591`) +- Bug in :func:`to_datetime` with an :class:`Index` argument that would drop the ``name`` from the result (:issue:`21697`) Timedelta ^^^^^^^^^ diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index 57387b9ea870a..4a5290a90313d 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -99,13 +99,13 @@ def _convert_and_box_cache(arg, cache_array, box, errors, name=None): result = Series(arg).map(cache_array) if box: if errors == 'ignore': - return Index(result) + return Index(result, name=name) else: return DatetimeIndex(result, name=name) return result.values -def _return_parsed_timezone_results(result, timezones, box, tz): +def _return_parsed_timezone_results(result, timezones, box, tz, name): """ Return results from array_strptime if a %z or %Z directive was passed. @@ -119,6 +119,9 @@ def _return_parsed_timezone_results(result, timezones, box, tz): True boxes result as an Index-like, False returns an ndarray tz : object None or pytz timezone object + name : string, default None + Name for a DatetimeIndex + Returns ------- tz_result : ndarray of parsed dates with timezone @@ -136,7 +139,7 @@ def _return_parsed_timezone_results(result, timezones, box, tz): in zip(result, timezones)]) if box: from pandas import Index - return Index(tz_results) + return Index(tz_results, name=name) return tz_results @@ -209,7 +212,7 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None, if box: if errors == 'ignore': from pandas import Index - return Index(result) + return Index(result, name=name) return DatetimeIndex(result, tz=tz, name=name) return result @@ -252,7 +255,7 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None, arg, format, exact=exact, errors=errors) if '%Z' in format or '%z' in format: return _return_parsed_timezone_results( - result, timezones, box, tz) + result, timezones, box, tz, name) except tslibs.OutOfBoundsDatetime: if errors == 'raise': raise diff --git a/pandas/tests/indexes/datetimes/test_tools.py b/pandas/tests/indexes/datetimes/test_tools.py index cc6db8f5854c8..3b7d6a709230b 100644 --- a/pandas/tests/indexes/datetimes/test_tools.py +++ b/pandas/tests/indexes/datetimes/test_tools.py @@ -233,6 +233,15 @@ def test_to_datetime_parse_timezone_malformed(self, offset): with pytest.raises(ValueError): pd.to_datetime([date], format=fmt) + def test_to_datetime_parse_timezone_keeps_name(self): + # GH 21697 + fmt = '%Y-%m-%d %H:%M:%S %z' + arg = pd.Index(['2010-01-01 12:00:00 Z'], name='foo') + result = pd.to_datetime(arg, format=fmt) + expected = pd.DatetimeIndex(['2010-01-01 12:00:00'], tz='UTC', + name='foo') + tm.assert_index_equal(result, expected) + class TestToDatetime(object): def test_to_datetime_pydatetime(self): @@ -765,6 +774,14 @@ def test_unit_rounding(self, cache): expected = pd.Timestamp('2015-06-19 19:55:31.877000093') assert result == expected + @pytest.mark.parametrize('cache', [True, False]) + def test_unit_ignore_keeps_name(self, cache): + # GH 21697 + expected = pd.Index([15e9] * 2, name='name') + result = pd.to_datetime(expected, errors='ignore', box=True, unit='s', + cache=cache) + tm.assert_index_equal(result, expected) + @pytest.mark.parametrize('cache', [True, False]) def test_dataframe(self, cache): From 6247da0db4835ff723126640145b4fad3ce17343 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 2 Oct 2018 08:50:41 -0500 Subject: [PATCH 80/87] Provide default implementation for `data_repated` (#22935) --- pandas/tests/extension/conftest.py | 20 +++++++++++++++---- .../tests/extension/decimal/test_decimal.py | 8 -------- pandas/tests/extension/test_categorical.py | 9 --------- pandas/tests/extension/test_integer.py | 8 -------- pandas/tests/extension/test_interval.py | 9 --------- 5 files changed, 16 insertions(+), 38 deletions(-) diff --git a/pandas/tests/extension/conftest.py b/pandas/tests/extension/conftest.py index 4bbbb7df2f399..8e397d228a5b6 100644 --- a/pandas/tests/extension/conftest.py +++ b/pandas/tests/extension/conftest.py @@ -31,12 +31,24 @@ def all_data(request, data, data_missing): @pytest.fixture -def data_repeated(): - """Return different versions of data for count times""" +def data_repeated(data): + """ + Generate many datasets. + + Parameters + ---------- + data : fixture implementing `data` + + Returns + ------- + Callable[[int], Generator]: + A callable that takes a `count` argument and + returns a generator yielding `count` datasets. + """ def gen(count): for _ in range(count): - yield NotImplementedError - yield gen + yield data + return gen @pytest.fixture diff --git a/pandas/tests/extension/decimal/test_decimal.py b/pandas/tests/extension/decimal/test_decimal.py index 03fdd25826b79..93b8ea786ef5b 100644 --- a/pandas/tests/extension/decimal/test_decimal.py +++ b/pandas/tests/extension/decimal/test_decimal.py @@ -30,14 +30,6 @@ def data_missing(): return DecimalArray([decimal.Decimal('NaN'), decimal.Decimal(1)]) -@pytest.fixture -def data_repeated(): - def gen(count): - for _ in range(count): - yield DecimalArray(make_data()) - yield gen - - @pytest.fixture def data_for_sorting(): return DecimalArray([decimal.Decimal('1'), diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py index 6c6cf80c16da6..ff66f53eab6f6 100644 --- a/pandas/tests/extension/test_categorical.py +++ b/pandas/tests/extension/test_categorical.py @@ -45,15 +45,6 @@ def data_missing(): return Categorical([np.nan, 'A']) -@pytest.fixture -def data_repeated(): - """Return different versions of data for count times""" - def gen(count): - for _ in range(count): - yield Categorical(make_data()) - yield gen - - @pytest.fixture def data_for_sorting(): return Categorical(['A', 'B', 'C'], categories=['C', 'A', 'B'], diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index 57e0922a0b7d9..7aa33006dadda 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -47,14 +47,6 @@ def data_missing(dtype): return integer_array([np.nan, 1], dtype=dtype) -@pytest.fixture -def data_repeated(data): - def gen(count): - for _ in range(count): - yield data - yield gen - - @pytest.fixture def data_for_sorting(dtype): return integer_array([1, 2, 0], dtype=dtype) diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 34b98f590df0d..7302c5757d144 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -47,15 +47,6 @@ def data_missing(): return IntervalArray.from_tuples([None, (0, 1)]) -@pytest.fixture -def data_repeated(): - """Return different versions of data for count times""" - def gen(count): - for _ in range(count): - yield IntervalArray(make_data()) - yield gen - - @pytest.fixture def data_for_sorting(): return IntervalArray.from_tuples([(1, 2), (2, 3), (0, 1)]) From 1d9f76c5055d1ef31ce76134e88b5568a119f498 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 2 Oct 2018 17:11:11 +0200 Subject: [PATCH 81/87] CLN: remove Index._to_embed (#22879) * CLN: remove Index._to_embed * pep8 --- pandas/core/indexes/base.py | 14 +------------- pandas/core/indexes/datetimes.py | 18 ++++-------------- pandas/core/indexes/period.py | 10 ---------- 3 files changed, 5 insertions(+), 37 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index b42bbdafcab45..af04a846ed787 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1114,7 +1114,7 @@ def to_series(self, index=None, name=None): if name is None: name = self.name - return Series(self._to_embed(), index=index, name=name) + return Series(self.values.copy(), index=index, name=name) def to_frame(self, index=True, name=None): """ @@ -1177,18 +1177,6 @@ def to_frame(self, index=True, name=None): result.index = self return result - def _to_embed(self, keep_tz=False, dtype=None): - """ - *this is an internal non-public method* - - return an array repr of this object, potentially casting to object - - """ - if dtype is not None: - return self.astype(dtype)._to_embed(keep_tz=keep_tz) - - return self.values.copy() - _index_shared_docs['astype'] = """ Create an Index with values cast to dtypes. The class of a new Index is determined by dtype. When conversion is impossible, a ValueError diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 9b00f21668bf5..a6cdaa0c2163a 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -665,23 +665,13 @@ def to_series(self, keep_tz=False, index=None, name=None): if name is None: name = self.name - return Series(self._to_embed(keep_tz), index=index, name=name) - - def _to_embed(self, keep_tz=False, dtype=None): - """ - return an array repr of this object, potentially casting to object - - This is for internal compat - """ - if dtype is not None: - return self.astype(dtype)._to_embed(keep_tz=keep_tz) - if keep_tz and self.tz is not None: - # preserve the tz & copy - return self.copy(deep=True) + values = self.copy(deep=True) + else: + values = self.values.copy() - return self.values.copy() + return Series(values, index=index, name=name) def to_period(self, freq=None): """ diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 0f86e18103e3c..969391569ce50 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -365,16 +365,6 @@ def __array_wrap__(self, result, context=None): # cannot pass _simple_new as it is return self._shallow_copy(result, freq=self.freq, name=self.name) - def _to_embed(self, keep_tz=False, dtype=None): - """ - return an array repr of this object, potentially casting to object - """ - - if dtype is not None: - return self.astype(dtype)._to_embed(keep_tz=keep_tz) - - return self.astype(object).values - @property def size(self): # Avoid materializing self._values From 9caf04836ad34ca17da7b86ba7120cca58ce142a Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 2 Oct 2018 13:25:22 -0500 Subject: [PATCH 82/87] CI: change windows vm image (#22948) --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c82dafa224961..5d473bfc5a38c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,8 +18,8 @@ jobs: - template: ci/azure/windows.yml parameters: name: Windows - vmImage: vs2017-win2017 + vmImage: vs2017-win2016 - template: ci/azure/windows-py27.yml parameters: name: WindowsPy27 - vmImage: vs2017-win2017 + vmImage: vs2017-win2016 From 1102a33d9776ed316cade079e22be6daa76c9e42 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 2 Oct 2018 22:31:36 +0200 Subject: [PATCH 83/87] DOC/CLN: clean-up shared_docs in generic.py (#20074) --- pandas/core/frame.py | 9 +++-- pandas/core/generic.py | 65 ++++++++++-------------------------- pandas/core/panel.py | 16 +++++++-- pandas/core/series.py | 5 +-- pandas/core/sparse/series.py | 7 ++-- 5 files changed, 44 insertions(+), 58 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index b4e8b4e3a6bec..15cebb88faea7 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -3629,7 +3629,8 @@ def align(self, other, join='outer', axis=None, level=None, copy=True, fill_axis=fill_axis, broadcast_axis=broadcast_axis) - @Appender(_shared_docs['reindex'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(NDFrame.reindex.__doc__) @rewrite_axis_style_signature('labels', [('method', None), ('copy', True), ('level', None), @@ -4479,7 +4480,8 @@ def f(vals): # ---------------------------------------------------------------------- # Sorting - @Appender(_shared_docs['sort_values'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(NDFrame.sort_values.__doc__) def sort_values(self, by, axis=0, ascending=True, inplace=False, kind='quicksort', na_position='last'): inplace = validate_bool_kwarg(inplace, 'inplace') @@ -4521,7 +4523,8 @@ def sort_values(self, by, axis=0, ascending=True, inplace=False, else: return self._constructor(new_data).__finalize__(self) - @Appender(_shared_docs['sort_index'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(NDFrame.sort_index.__doc__) def sort_index(self, axis=0, level=None, ascending=True, inplace=False, kind='quicksort', na_position='last', sort_remaining=True, by=None): diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 393e7caae5fab..8fed92f7ed6b9 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -643,7 +643,8 @@ def _set_axis(self, axis, labels): self._data.set_axis(axis, labels) self._clear_item_cache() - _shared_docs['transpose'] = """ + def transpose(self, *args, **kwargs): + """ Permute the dimensions of the %(klass)s Parameters @@ -663,9 +664,6 @@ def _set_axis(self, axis, labels): y : same as input """ - @Appender(_shared_docs['transpose'] % _shared_doc_kwargs) - def transpose(self, *args, **kwargs): - # construct the args axes, kwargs = self._construct_axes_from_arguments(args, kwargs, require_all=True) @@ -965,9 +963,8 @@ def swaplevel(self, i=-2, j=-1, axis=0): # ---------------------------------------------------------------------- # Rename - # TODO: define separate funcs for DataFrame, Series and Panel so you can - # get completion on keyword arguments. - _shared_docs['rename'] = """ + def rename(self, *args, **kwargs): + """ Alter axes input function or functions. Function / dict values must be unique (1-to-1). Labels not contained in a dict / Series will be left as-is. Extra labels listed don't throw an error. Alternatively, change @@ -975,13 +972,11 @@ def swaplevel(self, i=-2, j=-1, axis=0): Parameters ---------- - %(optional_mapper)s %(axes)s : scalar, list-like, dict-like or function, optional Scalar or list-like will alter the ``Series.name`` attribute, and raise on DataFrame or Panel. dict-like or functions are transformations to apply to that axis' values - %(optional_axis)s copy : boolean, default True Also copy underlying data inplace : boolean, default False @@ -1069,12 +1064,6 @@ def swaplevel(self, i=-2, j=-1, axis=0): See the :ref:`user guide ` for more. """ - - @Appender(_shared_docs['rename'] % dict(axes='axes keywords for this' - ' object', klass='NDFrame', - optional_mapper='', - optional_axis='')) - def rename(self, *args, **kwargs): axes, kwargs = self._construct_axes_from_arguments(args, kwargs) copy = kwargs.pop('copy', True) inplace = kwargs.pop('inplace', False) @@ -1127,8 +1116,6 @@ def f(x): else: return result.__finalize__(self) - rename.__doc__ = _shared_docs['rename'] - def rename_axis(self, mapper, axis=0, copy=True, inplace=False): """ Alter the name of the index or columns. @@ -3024,7 +3011,8 @@ def __delitem__(self, key): except KeyError: pass - _shared_docs['_take'] = """ + def _take(self, indices, axis=0, is_copy=True): + """ Return the elements in the given *positional* indices along an axis. This means that we are not indexing according to actual values in @@ -3055,9 +3043,6 @@ def __delitem__(self, key): numpy.ndarray.take numpy.take """ - - @Appender(_shared_docs['_take']) - def _take(self, indices, axis=0, is_copy=True): self._consolidate_inplace() new_data = self._data.take(indices, @@ -3072,7 +3057,8 @@ def _take(self, indices, axis=0, is_copy=True): return result - _shared_docs['take'] = """ + def take(self, indices, axis=0, convert=None, is_copy=True, **kwargs): + """ Return the elements in the given *positional* indices along an axis. This means that we are not indexing according to actual values in @@ -3155,9 +3141,6 @@ class max_speed 1 monkey mammal NaN 3 lion mammal 80.5 """ - - @Appender(_shared_docs['take']) - def take(self, indices, axis=0, convert=None, is_copy=True, **kwargs): if convert is not None: msg = ("The 'convert' parameter is deprecated " "and will be removed in a future version.") @@ -3580,7 +3563,9 @@ def add_suffix(self, suffix): mapper = {self._info_axis_name: f} return self.rename(**mapper) - _shared_docs['sort_values'] = """ + def sort_values(self, by=None, axis=0, ascending=True, inplace=False, + kind='quicksort', na_position='last'): + """ Sort by the values along either axis Parameters @@ -3665,17 +3650,12 @@ def add_suffix(self, suffix): 0 A 2 0 1 A 1 1 """ - - def sort_values(self, by=None, axis=0, ascending=True, inplace=False, - kind='quicksort', na_position='last'): - """ - NOT IMPLEMENTED: do not call this method, as sorting values is not - supported for Panel objects and will raise an error. - """ raise NotImplementedError("sort_values has not been implemented " "on Panel or Panel4D objects.") - _shared_docs['sort_index'] = """ + def sort_index(self, axis=0, level=None, ascending=True, inplace=False, + kind='quicksort', na_position='last', sort_remaining=True): + """ Sort object by labels (along an axis) Parameters @@ -3703,10 +3683,6 @@ def sort_values(self, by=None, axis=0, ascending=True, inplace=False, ------- sorted_obj : %(klass)s """ - - @Appender(_shared_docs['sort_index'] % dict(axes="axes", klass="NDFrame")) - def sort_index(self, axis=0, level=None, ascending=True, inplace=False, - kind='quicksort', na_position='last', sort_remaining=True): inplace = validate_bool_kwarg(inplace, 'inplace') axis = self._get_axis_number(axis) axis_name = self._get_axis_name(axis) @@ -3724,7 +3700,8 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, new_axis = labels.take(sort_index) return self.reindex(**{axis_name: new_axis}) - _shared_docs['reindex'] = """ + def reindex(self, *args, **kwargs): + """ Conform %(klass)s to new index with optional filling logic, placing NA/NaN in locations having no value in the previous index. A new object is produced unless the new index is equivalent to the current one and @@ -3920,14 +3897,8 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, ------- reindexed : %(klass)s """ - - # TODO: Decide if we care about having different examples for different - # kinds - - @Appender(_shared_docs['reindex'] % dict(axes="axes", klass="NDFrame", - optional_labels="", - optional_axis="")) - def reindex(self, *args, **kwargs): + # TODO: Decide if we care about having different examples for different + # kinds # construct the args axes, kwargs = self._construct_axes_from_arguments(args, kwargs) diff --git a/pandas/core/panel.py b/pandas/core/panel.py index 81d1e83ee6870..1e2d4000413bb 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -1215,7 +1215,8 @@ def _wrap_result(self, result, axis): return self._construct_return_type(result, axes) - @Appender(_shared_docs['reindex'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(NDFrame.reindex.__doc__) def reindex(self, *args, **kwargs): major = kwargs.pop("major", None) minor = kwargs.pop('minor', None) @@ -1236,7 +1237,8 @@ def reindex(self, *args, **kwargs): kwargs.pop('labels', None) return super(Panel, self).reindex(**kwargs) - @Appender(_shared_docs['rename'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(NDFrame.rename.__doc__) def rename(self, items=None, major_axis=None, minor_axis=None, **kwargs): major_axis = (major_axis if major_axis is not None else kwargs.pop('major', None)) @@ -1253,7 +1255,8 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True, copy=copy, limit=limit, fill_value=fill_value) - @Appender(_shared_docs['transpose'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(NDFrame.transpose.__doc__) def transpose(self, *args, **kwargs): # check if a list of axes was passed in instead as a # single *args element @@ -1536,6 +1539,13 @@ def _extract_axis(self, data, axis=0, intersect=False): return ensure_index(index) + def sort_values(self, *args, **kwargs): + """ + NOT IMPLEMENTED: do not call this method, as sorting values is not + supported for Panel objects and will raise an error. + """ + super(Panel, self).sort_values(*args, **kwargs) + Panel._setup_axes(axes=['items', 'major_axis', 'minor_axis'], info_axis=0, stat_axis=1, aliases={'major': 'major_axis', diff --git a/pandas/core/series.py b/pandas/core/series.py index 83f80c305c5eb..82198c2b3edd5 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -3496,7 +3496,8 @@ def rename(self, index=None, **kwargs): return self._set_name(index, inplace=kwargs.get('inplace')) return super(Series, self).rename(index=index, **kwargs) - @Appender(generic._shared_docs['reindex'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(generic.NDFrame.reindex.__doc__) def reindex(self, index=None, **kwargs): return super(Series, self).reindex(index=index, **kwargs) @@ -3680,7 +3681,7 @@ def memory_usage(self, index=True, deep=False): v += self.index.memory_usage(deep=deep) return v - @Appender(generic._shared_docs['_take']) + @Appender(generic.NDFrame._take.__doc__) def _take(self, indices, axis=0, is_copy=False): indices = ensure_platform_int(indices) diff --git a/pandas/core/sparse/series.py b/pandas/core/sparse/series.py index 8ac5d81f23bb2..97cd3a0a1fb6a 100644 --- a/pandas/core/sparse/series.py +++ b/pandas/core/sparse/series.py @@ -19,7 +19,7 @@ import pandas.core.indexes.base as ibase import pandas.core.ops as ops import pandas._libs.index as libindex -from pandas.util._decorators import Appender +from pandas.util._decorators import Appender, Substitution from pandas.core.sparse.array import ( make_sparse, SparseArray, @@ -563,7 +563,8 @@ def copy(self, deep=True): return self._constructor(new_data, sparse_index=self.sp_index, fill_value=self.fill_value).__finalize__(self) - @Appender(generic._shared_docs['reindex'] % _shared_doc_kwargs) + @Substitution(**_shared_doc_kwargs) + @Appender(generic.NDFrame.reindex.__doc__) def reindex(self, index=None, method=None, copy=True, limit=None, **kwargs): @@ -592,7 +593,7 @@ def sparse_reindex(self, new_index): sparse_index=new_index, fill_value=self.fill_value).__finalize__(self) - @Appender(generic._shared_docs['take']) + @Appender(generic.NDFrame.take.__doc__) def take(self, indices, axis=0, convert=None, *args, **kwargs): if convert is not None: msg = ("The 'convert' parameter is deprecated " From 8e749a33b5f814bded42044a4182449d5d6c8213 Mon Sep 17 00:00:00 2001 From: Pamela Wu Date: Tue, 2 Oct 2018 17:14:48 -0400 Subject: [PATCH 84/87] CLN GH22874 replace bare excepts in pandas/io/pytables.py (#22919) --- pandas/io/pytables.py | 49 ++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py index c57b1c3e211f6..fc9e415ed38f7 100644 --- a/pandas/io/pytables.py +++ b/pandas/io/pytables.py @@ -258,7 +258,7 @@ def _tables(): try: _table_file_open_policy_is_strict = ( tables.file._FILE_OPEN_POLICY == 'strict') - except: + except AttributeError: pass return _table_mod @@ -395,11 +395,11 @@ def read_hdf(path_or_buf, key=None, mode='r', **kwargs): 'contains multiple datasets.') key = candidate_only_group._v_pathname return store.select(key, auto_close=auto_close, **kwargs) - except: + except (ValueError, TypeError): # if there is an error, close the store try: store.close() - except: + except AttributeError: pass raise @@ -517,7 +517,7 @@ def __getattr__(self, name): """ allow attribute access to get stores """ try: return self.get(name) - except: + except (KeyError, ClosedFileError): pass raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, name)) @@ -675,7 +675,7 @@ def flush(self, fsync=False): if fsync: try: os.fsync(self._handle.fileno()) - except: + except OSError: pass def get(self, key): @@ -1161,7 +1161,7 @@ def get_node(self, key): if not key.startswith('/'): key = '/' + key return self._handle.get_node(self.root, key) - except: + except _table_mod.exceptions.NoSuchNodeError: return None def get_storer(self, key): @@ -1270,7 +1270,7 @@ def _validate_format(self, format, kwargs): # validate try: kwargs['format'] = _FORMAT_MAP[format.lower()] - except: + except KeyError: raise TypeError("invalid HDFStore format specified [{0}]" .format(format)) @@ -1307,7 +1307,7 @@ def error(t): try: pt = _TYPE_MAP[type(value)] - except: + except KeyError: error('_TYPE_MAP') # we are actually a table @@ -1318,7 +1318,7 @@ def error(t): if u('table') not in pt: try: return globals()[_STORER_MAP[pt]](self, group, **kwargs) - except: + except KeyError: error('_STORER_MAP') # existing node (and must be a table) @@ -1354,12 +1354,12 @@ def error(t): fields = group.table._v_attrs.fields if len(fields) == 1 and fields[0] == u('value'): tt = u('legacy_frame') - except: + except IndexError: pass try: return globals()[_TABLE_MAP[tt]](self, group, **kwargs) - except: + except KeyError: error('_TABLE_MAP') def _write_to_group(self, key, value, format, index=True, append=False, @@ -1624,7 +1624,7 @@ def is_indexed(self): """ return whether I am an indexed column """ try: return getattr(self.table.cols, self.cname).is_indexed - except: + except AttributeError: False def copy(self): @@ -1654,9 +1654,10 @@ def convert(self, values, nan_rep, encoding, errors): kwargs['freq'] = _ensure_decoded(self.freq) if self.index_name is not None: kwargs['name'] = _ensure_decoded(self.index_name) + # making an Index instance could throw a number of different errors try: self.values = Index(values, **kwargs) - except: + except Exception: # noqa: E722 # if the output freq is different that what we recorded, # it should be None (see also 'doc example part 2') @@ -1869,7 +1870,7 @@ def create_for_block( m = re.search(r"values_block_(\d+)", name) if m: name = "values_%s" % m.groups()[0] - except: + except IndexError: pass return cls(name=name, cname=cname, **kwargs) @@ -2232,7 +2233,7 @@ def convert(self, values, nan_rep, encoding, errors): try: self.data = self.data.astype(dtype, copy=False) - except: + except TypeError: self.data = self.data.astype('O', copy=False) # convert nans / decode @@ -2325,7 +2326,7 @@ def set_version(self): self.version = tuple(int(x) for x in version.split('.')) if len(self.version) == 2: self.version = self.version + (0,) - except: + except AttributeError: self.version = (0, 0, 0) @property @@ -2769,7 +2770,7 @@ def write_array(self, key, value, items=None): else: try: items = list(items) - except: + except TypeError: pass ws = performance_doc % (inferred_type, key, items) warnings.warn(ws, PerformanceWarning, stacklevel=7) @@ -2843,7 +2844,7 @@ class SeriesFixed(GenericFixed): def shape(self): try: return len(getattr(self.group, 'values')), - except: + except (TypeError, AttributeError): return None def read(self, **kwargs): @@ -2961,7 +2962,7 @@ def shape(self): shape = shape[::-1] return shape - except: + except AttributeError: return None def read(self, start=None, stop=None, **kwargs): @@ -3495,7 +3496,7 @@ def create_axes(self, axes, obj, validate=True, nan_rep=None, if axes is None: try: axes = _AXES_MAP[type(obj)] - except: + except KeyError: raise TypeError("cannot properly create the storer for: " "[group->%s,value->%s]" % (self.group._v_name, type(obj))) @@ -3614,7 +3615,7 @@ def get_blk_items(mgr, blocks): b, b_items = by_items.pop(items) new_blocks.append(b) new_blk_items.append(b_items) - except: + except (IndexError, KeyError): raise ValueError( "cannot match existing table structure for [%s] on " "appending data" % ','.join(pprint_thing(item) for @@ -3642,7 +3643,7 @@ def get_blk_items(mgr, blocks): if existing_table is not None and validate: try: existing_col = existing_table.values_axes[i] - except: + except (IndexError, KeyError): raise ValueError("Incompatible appended table [%s] with " "existing table [%s]" % (blocks, existing_table.values_axes)) @@ -4460,7 +4461,7 @@ def _get_info(info, name): """ get/create the info for this name """ try: idx = info[name] - except: + except KeyError: idx = info[name] = dict() return idx @@ -4782,7 +4783,7 @@ def __init__(self, table, where=None, start=None, stop=None, **kwargs): ) self.coordinates = where - except: + except ValueError: pass if self.coordinates is None: From c44bad24996f9e747f2119fa0c6a90d893f6e2aa Mon Sep 17 00:00:00 2001 From: Pamela Wu Date: Tue, 2 Oct 2018 17:16:25 -0400 Subject: [PATCH 85/87] CLN GH22873 Replace base excepts in pandas/core (#22901) --- doc/source/whatsnew/v0.24.0.txt | 1 - pandas/core/computation/pytables.py | 2 +- pandas/core/dtypes/common.py | 2 +- pandas/core/dtypes/dtypes.py | 8 ++++---- pandas/core/frame.py | 4 ++-- pandas/core/indexes/frozen.py | 2 +- pandas/core/indexes/multi.py | 12 +++++++----- pandas/core/indexing.py | 6 +++--- pandas/core/internals/blocks.py | 10 +++++----- pandas/core/nanops.py | 5 +++-- pandas/core/ops.py | 3 ++- pandas/core/sparse/array.py | 2 +- pandas/core/tools/datetimes.py | 10 +++++----- pandas/core/window.py | 2 +- 14 files changed, 36 insertions(+), 33 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 851c1a3fbd6e9..f83185173c3e3 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -834,4 +834,3 @@ Other - :meth:`DataFrame.nlargest` and :meth:`DataFrame.nsmallest` now returns the correct n values when keep != 'all' also when tied on the first columns (:issue:`22752`) - :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly. - Logical operations ``&, |, ^`` between :class:`Series` and :class:`Index` will no longer raise ``ValueError`` (:issue:`22092`) -- diff --git a/pandas/core/computation/pytables.py b/pandas/core/computation/pytables.py index 2bd1b0c5b3507..e08df3e340138 100644 --- a/pandas/core/computation/pytables.py +++ b/pandas/core/computation/pytables.py @@ -411,7 +411,7 @@ def visit_Subscript(self, node, **kwargs): slobj = self.visit(node.slice) try: value = value.value - except: + except AttributeError: pass try: diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index e2b9e246aee50..5f0b71d4505c2 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -467,7 +467,7 @@ def is_timedelta64_dtype(arr_or_dtype): return False try: tipo = _get_dtype_type(arr_or_dtype) - except: + except (TypeError, ValueError, SyntaxError): return False return issubclass(tipo, np.timedelta64) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index d879ded4f0f09..fe5cc9389a8ba 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -358,11 +358,11 @@ def construct_from_string(cls, string): try: if string == 'category': return cls() - except: + else: + raise TypeError("cannot construct a CategoricalDtype") + except AttributeError: pass - raise TypeError("cannot construct a CategoricalDtype") - @staticmethod def validate_ordered(ordered): """ @@ -519,7 +519,7 @@ def __new__(cls, unit=None, tz=None): if m is not None: unit = m.groupdict()['unit'] tz = m.groupdict()['tz'] - except: + except TypeError: raise ValueError("could not construct DatetimeTZDtype") elif isinstance(unit, compat.string_types): diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 15cebb88faea7..abe8a519afe1b 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -3260,7 +3260,7 @@ def _ensure_valid_index(self, value): if not len(self.index) and is_list_like(value): try: value = Series(value) - except: + except (ValueError, NotImplementedError, TypeError): raise ValueError('Cannot set a frame with no defined index ' 'and a value that cannot be converted to a ' 'Series') @@ -7750,7 +7750,7 @@ def convert(v): values = np.array([convert(v) for v in values]) else: values = convert(values) - except: + except (ValueError, TypeError): values = convert(values) else: diff --git a/pandas/core/indexes/frozen.py b/pandas/core/indexes/frozen.py index 5a37e03b700f9..289970aaf3a82 100644 --- a/pandas/core/indexes/frozen.py +++ b/pandas/core/indexes/frozen.py @@ -139,7 +139,7 @@ def searchsorted(self, value, side="left", sorter=None): # xref: https://github.com/numpy/numpy/issues/5370 try: value = self.dtype.type(value) - except: + except ValueError: pass return super(FrozenNDArray, self).searchsorted( diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 3e6b934e1e863..119a607fc0e68 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -6,6 +6,7 @@ import numpy as np from pandas._libs import algos as libalgos, index as libindex, lib, Timestamp +from pandas._libs import tslibs from pandas.compat import range, zip, lrange, lzip, map from pandas.compat.numpy import function as nv @@ -1002,12 +1003,13 @@ def _try_mi(k): return _try_mi(key) except (KeyError): raise - except: + except (IndexError, ValueError, TypeError): pass try: return _try_mi(Timestamp(key)) - except: + except (KeyError, TypeError, + IndexError, ValueError, tslibs.OutOfBoundsDatetime): pass raise InvalidIndexError(key) @@ -1686,7 +1688,7 @@ def append(self, other): # if all(isinstance(x, MultiIndex) for x in other): try: return MultiIndex.from_tuples(new_tuples, names=self.names) - except: + except (TypeError, IndexError): return Index(new_tuples) def argsort(self, *args, **kwargs): @@ -2315,7 +2317,7 @@ def maybe_droplevels(indexer, levels, drop_level): for i in sorted(levels, reverse=True): try: new_index = new_index.droplevel(i) - except: + except ValueError: # no dropping here return orig_index @@ -2818,7 +2820,7 @@ def _convert_can_do_setop(self, other): msg = 'other must be a MultiIndex or a list of tuples' try: other = MultiIndex.from_tuples(other) - except: + except TypeError: raise TypeError(msg) else: result_names = self.names if self.names == other.names else None diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index b63f874abff85..150518aadcfd9 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2146,7 +2146,7 @@ def _getitem_tuple(self, tup): self._has_valid_tuple(tup) try: return self._getitem_lowerdim(tup) - except: + except IndexingError: pass retval = self.obj @@ -2705,13 +2705,13 @@ def maybe_droplevels(index, key): for _ in key: try: index = index.droplevel(0) - except: + except ValueError: # we have dropped too much, so back out return original_index else: try: index = index.droplevel(0) - except: + except ValueError: pass return index diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 6576db9f642a6..0e57dd33b1c4e 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -666,7 +666,7 @@ def _astype(self, dtype, copy=False, errors='raise', values=None, newb = make_block(values, placement=self.mgr_locs, klass=klass, ndim=self.ndim) - except: + except Exception: # noqa: E722 if errors == 'raise': raise newb = self.copy() if copy else self @@ -1142,7 +1142,7 @@ def check_int_bool(self, inplace): # a fill na type method try: m = missing.clean_fill_method(method) - except: + except ValueError: m = None if m is not None: @@ -1157,7 +1157,7 @@ def check_int_bool(self, inplace): # try an interp method try: m = missing.clean_interp_method(method, **kwargs) - except: + except ValueError: m = None if m is not None: @@ -2438,7 +2438,7 @@ def set(self, locs, values, check=False): try: if (self.values[locs] == values).all(): return - except: + except (IndexError, ValueError): pass try: self.values[locs] = values @@ -3172,7 +3172,7 @@ def _astype(self, dtype, copy=False, errors='raise', values=None, def __len__(self): try: return self.sp_index.length - except: + except AttributeError: return 0 def copy(self, deep=True, mgr=None): diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py index 7619d47cbc8f9..232d030da7f1e 100644 --- a/pandas/core/nanops.py +++ b/pandas/core/nanops.py @@ -503,7 +503,8 @@ def reduction(values, axis=None, skipna=True): try: result = getattr(values, meth)(axis, dtype=dtype_max) result.fill(np.nan) - except: + except (AttributeError, TypeError, + ValueError, np.core._internal.AxisError): result = np.nan else: result = getattr(values, meth)(axis) @@ -815,7 +816,7 @@ def _ensure_numeric(x): elif is_object_dtype(x): try: x = x.astype(np.complex128) - except: + except (TypeError, ValueError): x = x.astype(np.float64) else: if not np.any(x.imag): diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 70fe7de0a973e..ad187b08e0742 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -1545,7 +1545,8 @@ def na_op(x, y): y = bool(y) try: result = libops.scalar_binop(x, y, op) - except: + except (TypeError, ValueError, AttributeError, + OverflowError, NotImplementedError): raise TypeError("cannot compare a dtyped [{dtype}] array " "with a scalar of type [{typ}]" .format(dtype=x.dtype, diff --git a/pandas/core/sparse/array.py b/pandas/core/sparse/array.py index eb07e5ef6c85f..186a2490a5f2e 100644 --- a/pandas/core/sparse/array.py +++ b/pandas/core/sparse/array.py @@ -306,7 +306,7 @@ def __setstate__(self, state): def __len__(self): try: return self.sp_index.length - except: + except AttributeError: return 0 def __unicode__(self): diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index 4a5290a90313d..eb8d2b0b6c809 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -244,7 +244,7 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None, if format == '%Y%m%d': try: result = _attempt_YYYYMMDD(arg, errors=errors) - except: + except (ValueError, TypeError, tslibs.OutOfBoundsDatetime): raise ValueError("cannot convert the input to " "'%Y%m%d' date format") @@ -334,7 +334,7 @@ def _adjust_to_origin(arg, origin, unit): raise ValueError("unit must be 'D' for origin='julian'") try: arg = arg - j0 - except: + except TypeError: raise ValueError("incompatible 'arg' type for given " "'origin'='julian'") @@ -731,21 +731,21 @@ def calc_with_mask(carg, mask): # try intlike / strings that are ints try: return calc(arg.astype(np.int64)) - except: + except ValueError: pass # a float with actual np.nan try: carg = arg.astype(np.float64) return calc_with_mask(carg, notna(carg)) - except: + except ValueError: pass # string with NaN-like try: mask = ~algorithms.isin(arg, list(tslib.nat_strings)) return calc_with_mask(arg, mask) - except: + except ValueError: pass return None diff --git a/pandas/core/window.py b/pandas/core/window.py index 5cdf62d5a5537..4281d66a640e3 100644 --- a/pandas/core/window.py +++ b/pandas/core/window.py @@ -2504,7 +2504,7 @@ def _offset(window, center): offset = (window - 1) / 2. if center else 0 try: return int(offset) - except: + except TypeError: return offset.astype(int) From 08ecba8dab4a35ad3cad89fe02c7240674938b97 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 2 Oct 2018 14:22:53 -0700 Subject: [PATCH 86/87] BUG: fix DataFrame+DataFrame op with timedelta64 dtype (#22696) --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/core/frame.py | 2 +- pandas/core/ops.py | 42 +++++++++++++++++++++++++-- pandas/tests/frame/test_arithmetic.py | 15 ++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index f83185173c3e3..9b71ab656920d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -666,7 +666,7 @@ Timedelta - Bug in :class:`Index` with numeric dtype when multiplying or dividing an array with dtype ``timedelta64`` (:issue:`22390`) - Bug in :class:`TimedeltaIndex` incorrectly allowing indexing with ``Timestamp`` object (:issue:`20464`) - Fixed bug where subtracting :class:`Timedelta` from an object-dtyped array would raise ``TypeError`` (:issue:`21980`) -- +- Fixed bug in adding a :class:`DataFrame` with all-`timedelta64[ns]` dtypes to a :class:`DataFrame` with all-integer dtypes returning incorrect results instead of raising ``TypeError`` (:issue:`22696`) - Timezones diff --git a/pandas/core/frame.py b/pandas/core/frame.py index abe8a519afe1b..138d1017aa43d 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4889,7 +4889,7 @@ def _arith_op(left, right): left, right = ops.fill_binop(left, right, fill_value) return func(left, right) - if this._is_mixed_type or other._is_mixed_type: + if ops.should_series_dispatch(this, other, func): # iterate over columns return ops.dispatch_to_series(this, other, _arith_op) else: diff --git a/pandas/core/ops.py b/pandas/core/ops.py index ad187b08e0742..8171840c96b6e 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -900,6 +900,42 @@ def invalid_comparison(left, right, op): return res_values +# ----------------------------------------------------------------------------- +# Dispatch logic + +def should_series_dispatch(left, right, op): + """ + Identify cases where a DataFrame operation should dispatch to its + Series counterpart. + + Parameters + ---------- + left : DataFrame + right : DataFrame + op : binary operator + + Returns + ------- + override : bool + """ + if left._is_mixed_type or right._is_mixed_type: + return True + + if not len(left.columns) or not len(right.columns): + # ensure obj.dtypes[0] exists for each obj + return False + + ldtype = left.dtypes.iloc[0] + rdtype = right.dtypes.iloc[0] + + if ((is_timedelta64_dtype(ldtype) and is_integer_dtype(rdtype)) or + (is_timedelta64_dtype(rdtype) and is_integer_dtype(ldtype))): + # numpy integer dtypes as timedelta64 dtypes in this scenario + return True + + return False + + # ----------------------------------------------------------------------------- # Functions that add arithmetic methods to objects, given arithmetic factory # methods @@ -1803,8 +1839,10 @@ def f(self, other, axis=default_axis, level=None, fill_value=None): other = _align_method_FRAME(self, other, axis) - if isinstance(other, ABCDataFrame): # Another DataFrame - return self._combine_frame(other, na_op, fill_value, level) + if isinstance(other, ABCDataFrame): + # Another DataFrame + pass_op = op if should_series_dispatch(self, other, op) else na_op + return self._combine_frame(other, pass_op, fill_value, level) elif isinstance(other, ABCSeries): return _combine_series_frame(self, other, na_op, fill_value=fill_value, axis=axis, diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 2b08897864db0..2eb11c3a2e2f7 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -266,3 +266,18 @@ def test_df_bool_mul_int(self): result = 1 * df kinds = result.dtypes.apply(lambda x: x.kind) assert (kinds == 'i').all() + + def test_td64_df_add_int_frame(self): + # GH#22696 Check that we don't dispatch to numpy implementation, + # which treats int64 as m8[ns] + tdi = pd.timedelta_range('1', periods=3) + df = tdi.to_frame() + other = pd.DataFrame([1, 2, 3], index=tdi) # indexed like `df` + with pytest.raises(TypeError): + df + other + with pytest.raises(TypeError): + other + df + with pytest.raises(TypeError): + df - other + with pytest.raises(TypeError): + other - df From b0f9a104f323d687a56ea878ff78ff005f37b42d Mon Sep 17 00:00:00 2001 From: Tony Tao <34781056+tonytao2012@users.noreply.github.com> Date: Tue, 2 Oct 2018 19:01:08 -0500 Subject: [PATCH 87/87] DOC GH22893 Fix docstring of groupby in pandas/core/generic.py (#22920) --- pandas/core/generic.py | 101 +++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 8fed92f7ed6b9..cc157cc7228a8 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -7034,8 +7034,12 @@ def clip_lower(self, threshold, axis=None, inplace=False): def groupby(self, by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False, observed=False, **kwargs): """ - Group series using mapper (dict or key function, apply given function - to group, return result as series) or by a series of columns. + Group DataFrame or Series using a mapper or by a Series of columns. + + A groupby operation involves some combination of splitting the + object, applying a function, and combining the results. This can be + used to group large amounts of data and compute operations on these + groups. Parameters ---------- @@ -7048,54 +7052,95 @@ def groupby(self, by=None, axis=0, level=None, as_index=True, sort=True, values are used as-is determine the groups. A label or list of labels may be passed to group by the columns in ``self``. Notice that a tuple is interpreted a (single) key. - axis : int, default 0 + axis : {0 or 'index', 1 or 'columns'}, default 0 + Split along rows (0) or columns (1). level : int, level name, or sequence of such, default None If the axis is a MultiIndex (hierarchical), group by a particular - level or levels - as_index : boolean, default True + level or levels. + as_index : bool, default True For aggregated output, return object with group labels as the index. Only relevant for DataFrame input. as_index=False is - effectively "SQL-style" grouped output - sort : boolean, default True + effectively "SQL-style" grouped output. + sort : bool, default True Sort group keys. Get better performance by turning this off. Note this does not influence the order of observations within each - group. groupby preserves the order of rows within each group. - group_keys : boolean, default True - When calling apply, add group keys to index to identify pieces - squeeze : boolean, default False - reduce the dimensionality of the return type if possible, - otherwise return a consistent type - observed : boolean, default False - This only applies if any of the groupers are Categoricals + group. Groupby preserves the order of rows within each group. + group_keys : bool, default True + When calling apply, add group keys to index to identify pieces. + squeeze : bool, default False + Reduce the dimensionality of the return type if possible, + otherwise return a consistent type. + observed : bool, default False + This only applies if any of the groupers are Categoricals. If True: only show observed values for categorical groupers. If False: show all values for categorical groupers. .. versionadded:: 0.23.0 + **kwargs + Optional, only accepts keyword argument 'mutated' and is passed + to groupby. + Returns ------- - GroupBy object + DataFrameGroupBy or SeriesGroupBy + Depends on the calling object and returns groupby object that + contains information about the groups. - Examples + See Also -------- - DataFrame results - - >>> data.groupby(func, axis=0).mean() - >>> data.groupby(['col1', 'col2'])['col3'].mean() - - DataFrame with hierarchical index - - >>> data.groupby(['col1', 'col2']).mean() + resample : Convenience method for frequency conversion and resampling + of time series. Notes ----- See the `user guide `_ for more. - See also + Examples -------- - resample : Convenience method for frequency conversion and resampling - of time series. + >>> df = pd.DataFrame({'Animal' : ['Falcon', 'Falcon', + ... 'Parrot', 'Parrot'], + ... 'Max Speed' : [380., 370., 24., 26.]}) + >>> df + Animal Max Speed + 0 Falcon 380.0 + 1 Falcon 370.0 + 2 Parrot 24.0 + 3 Parrot 26.0 + >>> df.groupby(['Animal']).mean() + Max Speed + Animal + Falcon 375.0 + Parrot 25.0 + + **Hierarchical Indexes** + + We can groupby different levels of a hierarchical index + using the `level` parameter: + + >>> arrays = [['Falcon', 'Falcon', 'Parrot', 'Parrot'], + ... ['Capitve', 'Wild', 'Capitve', 'Wild']] + >>> index = pd.MultiIndex.from_arrays(arrays, names=('Animal', 'Type')) + >>> df = pd.DataFrame({'Max Speed' : [390., 350., 30., 20.]}, + ... index=index) + >>> df + Max Speed + Animal Type + Falcon Capitve 390.0 + Wild 350.0 + Parrot Capitve 30.0 + Wild 20.0 + >>> df.groupby(level=0).mean() + Max Speed + Animal + Falcon 370.0 + Parrot 25.0 + >>> df.groupby(level=1).mean() + Max Speed + Type + Capitve 210.0 + Wild 185.0 """ from pandas.core.groupby.groupby import groupby