diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d87fa5203bd52..a337ccbc98650 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,6 +154,39 @@ jobs: echo "region = BHS" >> $RCLONE_CONFIG_PATH if: github.event_name == 'push' - - name: Sync web + - name: Sync web with OVH run: rclone sync pandas_web ovh_cloud_pandas_web:dev if: github.event_name == 'push' + + - name: Create git repo to upload the built docs to GitHub pages + run: | + cd pandas_web + git init + touch .nojekyll + echo "dev.pandas.io" > CNAME + printf "User-agent: *\nDisallow: /" > robots.txt + git add --all . + git config user.email "pandas-dev@python.org" + git config user.name "pandas-bot" + git commit -m "pandas web and documentation in master" + if: github.event_name == 'push' + + # For this task to work, next steps are required: + # 1. Generate a pair of private/public keys (i.e. `ssh-keygen -t rsa -b 4096 -C "your_email@example.com"`) + # 2. Go to https://github.com/pandas-dev/pandas/settings/secrets + # 3. Click on "Add a new secret" + # 4. Name: "github_pagas_ssh_key", Value: + # 5. The public key needs to be upladed to https://github.com/pandas-dev/pandas-dev.github.io/settings/keys + - name: Install GitHub pages ssh deployment key + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.github_pages_ssh_key }} + known_hosts: 'github.com,192.30.252.128 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' + if: github.event_name == 'push' + + - name: Publish web and docs to GitHub pages + run: | + cd pandas_web + git remote add origin git@github.com:pandas-dev/pandas-dev.github.io.git + git push -f origin master || true + if: github.event_name == 'push' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 139b9e31df46c..896765722bf32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,20 @@ repos: - id: flake8 language: python_venv additional_dependencies: [flake8-comprehensions>=3.1.0] + - id: flake8 + name: flake8-pyx + language: python_venv + files: \.(pyx|pxd)$ + types: + - file + args: [--append-config=flake8/cython.cfg] + - id: flake8 + name: flake8-pxd + language: python_venv + files: \.pxi\.in$ + types: + - file + args: [--append-config=flake8/cython-template.cfg] - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: diff --git a/asv_bench/benchmarks/algorithms.py b/asv_bench/benchmarks/algorithms.py index 0f3b3838de1b2..1768e682b3db4 100644 --- a/asv_bench/benchmarks/algorithms.py +++ b/asv_bench/benchmarks/algorithms.py @@ -31,83 +31,62 @@ def time_maybe_convert_objects(self): class Factorize: - params = [[True, False], ["int", "uint", "float", "string"]] - param_names = ["sort", "dtype"] - - def setup(self, sort, dtype): - N = 10 ** 5 - data = { - "int": pd.Int64Index(np.arange(N).repeat(5)), - "uint": pd.UInt64Index(np.arange(N).repeat(5)), - "float": pd.Float64Index(np.random.randn(N).repeat(5)), - "string": tm.makeStringIndex(N).repeat(5), - } - self.idx = data[dtype] - - def time_factorize(self, sort, dtype): - self.idx.factorize(sort=sort) - - -class FactorizeUnique: - - params = [[True, False], ["int", "uint", "float", "string"]] - param_names = ["sort", "dtype"] + params = [ + [True, False], + [True, False], + ["int", "uint", "float", "string", "datetime64[ns]", "datetime64[ns, tz]"], + ] + param_names = ["unique", "sort", "dtype"] - def setup(self, sort, dtype): + def setup(self, unique, sort, dtype): N = 10 ** 5 data = { "int": pd.Int64Index(np.arange(N)), "uint": pd.UInt64Index(np.arange(N)), - "float": pd.Float64Index(np.arange(N)), + "float": pd.Float64Index(np.random.randn(N)), "string": tm.makeStringIndex(N), - } - self.idx = data[dtype] - assert self.idx.is_unique - - def time_factorize(self, sort, dtype): + "datetime64[ns]": pd.date_range("2011-01-01", freq="H", periods=N), + "datetime64[ns, tz]": pd.date_range( + "2011-01-01", freq="H", periods=N, tz="Asia/Tokyo" + ), + }[dtype] + if not unique: + data = data.repeat(5) + self.idx = data + + def time_factorize(self, unique, sort, dtype): self.idx.factorize(sort=sort) class Duplicated: - params = [["first", "last", False], ["int", "uint", "float", "string"]] - param_names = ["keep", "dtype"] - - def setup(self, keep, dtype): - N = 10 ** 5 - data = { - "int": pd.Int64Index(np.arange(N).repeat(5)), - "uint": pd.UInt64Index(np.arange(N).repeat(5)), - "float": pd.Float64Index(np.random.randn(N).repeat(5)), - "string": tm.makeStringIndex(N).repeat(5), - } - self.idx = data[dtype] - # cache is_unique - self.idx.is_unique - - def time_duplicated(self, keep, dtype): - self.idx.duplicated(keep=keep) - - -class DuplicatedUniqueIndex: - - params = ["int", "uint", "float", "string"] - param_names = ["dtype"] + params = [ + [True, False], + ["first", "last", False], + ["int", "uint", "float", "string", "datetime64[ns]", "datetime64[ns, tz]"], + ] + param_names = ["unique", "keep", "dtype"] - def setup(self, dtype): + def setup(self, unique, keep, dtype): N = 10 ** 5 data = { "int": pd.Int64Index(np.arange(N)), "uint": pd.UInt64Index(np.arange(N)), "float": pd.Float64Index(np.random.randn(N)), "string": tm.makeStringIndex(N), - } - self.idx = data[dtype] + "datetime64[ns]": pd.date_range("2011-01-01", freq="H", periods=N), + "datetime64[ns, tz]": pd.date_range( + "2011-01-01", freq="H", periods=N, tz="Asia/Tokyo" + ), + }[dtype] + if not unique: + data = data.repeat(5) + self.idx = data # cache is_unique self.idx.is_unique - def time_duplicated_unique(self, dtype): - self.idx.duplicated() + def time_duplicated(self, unique, keep, dtype): + self.idx.duplicated(keep=keep) class Hashing: diff --git a/asv_bench/benchmarks/binary_ops.py b/asv_bench/benchmarks/arithmetic.py similarity index 51% rename from asv_bench/benchmarks/binary_ops.py rename to asv_bench/benchmarks/arithmetic.py index 64e067d25a454..d1e94f62967f4 100644 --- a/asv_bench/benchmarks/binary_ops.py +++ b/asv_bench/benchmarks/arithmetic.py @@ -1,14 +1,23 @@ import operator +import warnings import numpy as np -from pandas import DataFrame, Series, date_range +import pandas as pd +from pandas import DataFrame, Series, Timestamp, date_range, to_timedelta +import pandas._testing as tm from pandas.core.algorithms import checked_add_with_arr +from .pandas_vb_common import numeric_dtypes + try: import pandas.core.computation.expressions as expr except ImportError: import pandas.computation.expressions as expr +try: + import pandas.tseries.holiday +except ImportError: + pass class IntFrameWithScalar: @@ -151,6 +160,110 @@ def time_timestamp_ops_diff_with_shift(self, tz): self.s - self.s.shift() +class IrregularOps: + def setup(self): + N = 10 ** 5 + idx = date_range(start="1/1/2000", periods=N, freq="s") + s = Series(np.random.randn(N), index=idx) + self.left = s.sample(frac=1) + self.right = s.sample(frac=1) + + def time_add(self): + self.left + self.right + + +class TimedeltaOps: + def setup(self): + self.td = to_timedelta(np.arange(1000000)) + self.ts = Timestamp("2000") + + def time_add_td_ts(self): + self.td + self.ts + + +class CategoricalComparisons: + params = ["__lt__", "__le__", "__eq__", "__ne__", "__ge__", "__gt__"] + param_names = ["op"] + + def setup(self, op): + N = 10 ** 5 + self.cat = pd.Categorical(list("aabbcd") * N, ordered=True) + + def time_categorical_op(self, op): + getattr(self.cat, op)("b") + + +class IndexArithmetic: + + params = ["float", "int"] + param_names = ["dtype"] + + def setup(self, dtype): + N = 10 ** 6 + indexes = {"int": "makeIntIndex", "float": "makeFloatIndex"} + self.index = getattr(tm, indexes[dtype])(N) + + def time_add(self, dtype): + self.index + 2 + + def time_subtract(self, dtype): + self.index - 2 + + def time_multiply(self, dtype): + self.index * 2 + + def time_divide(self, dtype): + self.index / 2 + + def time_modulo(self, dtype): + self.index % 2 + + +class NumericInferOps: + # from GH 7332 + params = numeric_dtypes + param_names = ["dtype"] + + def setup(self, dtype): + N = 5 * 10 ** 5 + self.df = DataFrame( + {"A": np.arange(N).astype(dtype), "B": np.arange(N).astype(dtype)} + ) + + def time_add(self, dtype): + self.df["A"] + self.df["B"] + + def time_subtract(self, dtype): + self.df["A"] - self.df["B"] + + def time_multiply(self, dtype): + self.df["A"] * self.df["B"] + + def time_divide(self, dtype): + self.df["A"] / self.df["B"] + + def time_modulo(self, dtype): + self.df["A"] % self.df["B"] + + +class DateInferOps: + # from GH 7332 + def setup_cache(self): + N = 5 * 10 ** 5 + df = DataFrame({"datetime64": np.arange(N).astype("datetime64[ms]")}) + df["timedelta"] = df["datetime64"] - df["datetime64"] + return df + + def time_subtract_datetimes(self, df): + df["datetime64"] - df["datetime64"] + + def time_timedelta_plus_datetime(self, df): + df["timedelta"] + df["datetime64"] + + def time_add_timedeltas(self, df): + df["timedelta"] + df["timedelta"] + + class AddOverflowScalar: params = [1, -1, 0] @@ -188,4 +301,68 @@ def time_add_overflow_both_arg_nan(self): ) +hcal = pd.tseries.holiday.USFederalHolidayCalendar() +# These offsets currently raise a NotImplimentedError with .apply_index() +non_apply = [ + pd.offsets.Day(), + pd.offsets.BYearEnd(), + pd.offsets.BYearBegin(), + pd.offsets.BQuarterEnd(), + pd.offsets.BQuarterBegin(), + pd.offsets.BMonthEnd(), + pd.offsets.BMonthBegin(), + pd.offsets.CustomBusinessDay(), + pd.offsets.CustomBusinessDay(calendar=hcal), + pd.offsets.CustomBusinessMonthBegin(calendar=hcal), + pd.offsets.CustomBusinessMonthEnd(calendar=hcal), + pd.offsets.CustomBusinessMonthEnd(calendar=hcal), +] +other_offsets = [ + pd.offsets.YearEnd(), + pd.offsets.YearBegin(), + pd.offsets.QuarterEnd(), + pd.offsets.QuarterBegin(), + pd.offsets.MonthEnd(), + pd.offsets.MonthBegin(), + pd.offsets.DateOffset(months=2, days=2), + pd.offsets.BusinessDay(), + pd.offsets.SemiMonthEnd(), + pd.offsets.SemiMonthBegin(), +] +offsets = non_apply + other_offsets + + +class OffsetArrayArithmetic: + + params = offsets + param_names = ["offset"] + + def setup(self, offset): + N = 10000 + rng = pd.date_range(start="1/1/2000", periods=N, freq="T") + self.rng = rng + self.ser = pd.Series(rng) + + def time_add_series_offset(self, offset): + with warnings.catch_warnings(record=True): + self.ser + offset + + def time_add_dti_offset(self, offset): + with warnings.catch_warnings(record=True): + self.rng + offset + + +class ApplyIndex: + params = other_offsets + param_names = ["offset"] + + def setup(self, offset): + N = 10000 + rng = pd.date_range(start="1/1/2000", periods=N, freq="T") + self.rng = rng + + def time_apply_index(self, offset): + offset.apply_index(self.rng) + + from .pandas_vb_common import setup # noqa: F401 isort:skip diff --git a/asv_bench/benchmarks/categoricals.py b/asv_bench/benchmarks/categoricals.py index 1dcd52ac074a6..107b9b9edcd5d 100644 --- a/asv_bench/benchmarks/categoricals.py +++ b/asv_bench/benchmarks/categoricals.py @@ -63,18 +63,6 @@ def time_existing_series(self): pd.Categorical(self.series) -class CategoricalOps: - params = ["__lt__", "__le__", "__eq__", "__ne__", "__ge__", "__gt__"] - param_names = ["op"] - - def setup(self, op): - N = 10 ** 5 - self.cat = pd.Categorical(list("aabbcd") * N, ordered=True) - - def time_categorical_op(self, op): - getattr(self.cat, op)("b") - - class Concat: def setup(self): N = 10 ** 5 @@ -270,9 +258,6 @@ def setup(self): def time_get_loc(self): self.index.get_loc(self.category) - def time_shape(self): - self.index.shape - def time_shallow_copy(self): self.index._shallow_copy() diff --git a/asv_bench/benchmarks/index_cached_properties.py b/asv_bench/benchmarks/index_cached_properties.py index 13b33855569c9..16fbc741775e4 100644 --- a/asv_bench/benchmarks/index_cached_properties.py +++ b/asv_bench/benchmarks/index_cached_properties.py @@ -7,6 +7,7 @@ class IndexCache: params = [ [ + "CategoricalIndex", "DatetimeIndex", "Float64Index", "IntervalIndex", @@ -42,6 +43,8 @@ def setup(self, index_type): self.idx = pd.Float64Index(range(N)) elif index_type == "UInt64Index": self.idx = pd.UInt64Index(range(N)) + elif index_type == "CategoricalIndex": + self.idx = pd.CategoricalIndex(range(N), range(N)) else: raise ValueError assert len(self.idx) == N diff --git a/asv_bench/benchmarks/index_object.py b/asv_bench/benchmarks/index_object.py index 103141545504b..b242de6a17208 100644 --- a/asv_bench/benchmarks/index_object.py +++ b/asv_bench/benchmarks/index_object.py @@ -55,40 +55,6 @@ def time_datetime_difference_disjoint(self): self.datetime_left.difference(self.datetime_right) -class Datetime: - def setup(self): - self.dr = date_range("20000101", freq="D", periods=10000) - - def time_is_dates_only(self): - self.dr._is_dates_only - - -class Ops: - - params = ["float", "int"] - param_names = ["dtype"] - - def setup(self, dtype): - N = 10 ** 6 - indexes = {"int": "makeIntIndex", "float": "makeFloatIndex"} - self.index = getattr(tm, indexes[dtype])(N) - - def time_add(self, dtype): - self.index + 2 - - def time_subtract(self, dtype): - self.index - 2 - - def time_multiply(self, dtype): - self.index * 2 - - def time_divide(self, dtype): - self.index / 2 - - def time_modulo(self, dtype): - self.index % 2 - - class Range: def setup(self): self.idx_inc = RangeIndex(start=0, stop=10 ** 7, step=3) diff --git a/asv_bench/benchmarks/indexing.py b/asv_bench/benchmarks/indexing.py index 087fe3916845b..e98d2948e76ea 100644 --- a/asv_bench/benchmarks/indexing.py +++ b/asv_bench/benchmarks/indexing.py @@ -1,3 +1,8 @@ +""" +These benchmarks are for Series and DataFrame indexing methods. For the +lower-level methods directly on Index and subclasses, see index_object.py, +indexing_engine.py, and index_cached.py +""" import warnings import numpy as np diff --git a/asv_bench/benchmarks/inference.py b/asv_bench/benchmarks/inference.py index 1a8d5ede52512..40b064229ae49 100644 --- a/asv_bench/benchmarks/inference.py +++ b/asv_bench/benchmarks/inference.py @@ -1,53 +1,8 @@ import numpy as np -from pandas import DataFrame, Series, to_numeric +from pandas import Series, to_numeric -from .pandas_vb_common import lib, numeric_dtypes, tm - - -class NumericInferOps: - # from GH 7332 - params = numeric_dtypes - param_names = ["dtype"] - - def setup(self, dtype): - N = 5 * 10 ** 5 - self.df = DataFrame( - {"A": np.arange(N).astype(dtype), "B": np.arange(N).astype(dtype)} - ) - - def time_add(self, dtype): - self.df["A"] + self.df["B"] - - def time_subtract(self, dtype): - self.df["A"] - self.df["B"] - - def time_multiply(self, dtype): - self.df["A"] * self.df["B"] - - def time_divide(self, dtype): - self.df["A"] / self.df["B"] - - def time_modulo(self, dtype): - self.df["A"] % self.df["B"] - - -class DateInferOps: - # from GH 7332 - def setup_cache(self): - N = 5 * 10 ** 5 - df = DataFrame({"datetime64": np.arange(N).astype("datetime64[ms]")}) - df["timedelta"] = df["datetime64"] - df["datetime64"] - return df - - def time_subtract_datetimes(self, df): - df["datetime64"] - df["datetime64"] - - def time_timedelta_plus_datetime(self, df): - df["timedelta"] + df["datetime64"] - - def time_add_timedeltas(self, df): - df["timedelta"] + df["timedelta"] +from .pandas_vb_common import lib, tm class ToNumeric: diff --git a/asv_bench/benchmarks/multiindex_object.py b/asv_bench/benchmarks/multiindex_object.py index 0e188c58012fa..793f0c7c03c77 100644 --- a/asv_bench/benchmarks/multiindex_object.py +++ b/asv_bench/benchmarks/multiindex_object.py @@ -160,4 +160,43 @@ def time_equals_non_object_index(self): self.mi_large_slow.equals(self.idx_non_object) +class SetOperations: + + params = [ + ("monotonic", "non_monotonic"), + ("datetime", "int", "string"), + ("intersection", "union", "symmetric_difference"), + ] + param_names = ["index_structure", "dtype", "method"] + + def setup(self, index_structure, dtype, method): + N = 10 ** 5 + level1 = range(1000) + + level2 = date_range(start="1/1/2000", periods=N // 1000) + dates_left = MultiIndex.from_product([level1, level2]) + + level2 = range(N // 1000) + int_left = MultiIndex.from_product([level1, level2]) + + level2 = tm.makeStringIndex(N // 1000).values + str_left = MultiIndex.from_product([level1, level2]) + + data = { + "datetime": dates_left, + "int": int_left, + "string": str_left, + } + + if index_structure == "non_monotonic": + data = {k: mi[::-1] for k, mi in data.items()} + + data = {k: {"left": mi, "right": mi[:-1]} for k, mi in data.items()} + self.left = data[dtype]["left"] + self.right = data[dtype]["right"] + + def time_operation(self, index_structure, dtype, method): + getattr(self.left, method)(self.right) + + from .pandas_vb_common import setup # noqa: F401 isort:skip diff --git a/asv_bench/benchmarks/offset.py b/asv_bench/benchmarks/offset.py deleted file mode 100644 index 77ce1b2763bce..0000000000000 --- a/asv_bench/benchmarks/offset.py +++ /dev/null @@ -1,80 +0,0 @@ -import warnings - -import pandas as pd - -try: - import pandas.tseries.holiday -except ImportError: - pass - -hcal = pd.tseries.holiday.USFederalHolidayCalendar() -# These offsets currently raise a NotImplimentedError with .apply_index() -non_apply = [ - pd.offsets.Day(), - pd.offsets.BYearEnd(), - pd.offsets.BYearBegin(), - pd.offsets.BQuarterEnd(), - pd.offsets.BQuarterBegin(), - pd.offsets.BMonthEnd(), - pd.offsets.BMonthBegin(), - pd.offsets.CustomBusinessDay(), - pd.offsets.CustomBusinessDay(calendar=hcal), - pd.offsets.CustomBusinessMonthBegin(calendar=hcal), - pd.offsets.CustomBusinessMonthEnd(calendar=hcal), - pd.offsets.CustomBusinessMonthEnd(calendar=hcal), -] -other_offsets = [ - pd.offsets.YearEnd(), - pd.offsets.YearBegin(), - pd.offsets.QuarterEnd(), - pd.offsets.QuarterBegin(), - pd.offsets.MonthEnd(), - pd.offsets.MonthBegin(), - pd.offsets.DateOffset(months=2, days=2), - pd.offsets.BusinessDay(), - pd.offsets.SemiMonthEnd(), - pd.offsets.SemiMonthBegin(), -] -offsets = non_apply + other_offsets - - -class ApplyIndex: - - params = other_offsets - param_names = ["offset"] - - def setup(self, offset): - N = 10000 - self.rng = pd.date_range(start="1/1/2000", periods=N, freq="T") - - def time_apply_index(self, offset): - offset.apply_index(self.rng) - - -class OffsetSeriesArithmetic: - - params = offsets - param_names = ["offset"] - - def setup(self, offset): - N = 1000 - rng = pd.date_range(start="1/1/2000", periods=N, freq="T") - self.data = pd.Series(rng) - - def time_add_offset(self, offset): - with warnings.catch_warnings(record=True): - self.data + offset - - -class OffsetDatetimeIndexArithmetic: - - params = offsets - param_names = ["offset"] - - def setup(self, offset): - N = 1000 - self.data = pd.date_range(start="1/1/2000", periods=N, freq="T") - - def time_add_offset(self, offset): - with warnings.catch_warnings(record=True): - self.data + offset diff --git a/asv_bench/benchmarks/period.py b/asv_bench/benchmarks/period.py index b52aa2e55af35..e15d4c66e4fc0 100644 --- a/asv_bench/benchmarks/period.py +++ b/asv_bench/benchmarks/period.py @@ -85,9 +85,6 @@ def setup(self): def time_get_loc(self): self.index.get_loc(self.period) - def time_shape(self): - self.index.shape - def time_shallow_copy(self): self.index._shallow_copy() diff --git a/asv_bench/benchmarks/timedelta.py b/asv_bench/benchmarks/timedelta.py index 37418d752f833..cfe05c3e257b1 100644 --- a/asv_bench/benchmarks/timedelta.py +++ b/asv_bench/benchmarks/timedelta.py @@ -5,7 +5,7 @@ import numpy as np -from pandas import DataFrame, Series, Timestamp, timedelta_range, to_timedelta +from pandas import DataFrame, Series, timedelta_range, to_timedelta class ToTimedelta: @@ -41,15 +41,6 @@ def time_convert(self, errors): to_timedelta(self.arr, errors=errors) -class TimedeltaOps: - def setup(self): - self.td = to_timedelta(np.arange(1000000)) - self.ts = Timestamp("2000") - - def time_add_td_ts(self): - self.td + self.ts - - class DatetimeAccessor: def setup_cache(self): N = 100000 @@ -82,9 +73,6 @@ def setup(self): def time_get_loc(self): self.index.get_loc(self.timedelta) - def time_shape(self): - self.index.shape - def time_shallow_copy(self): self.index._shallow_copy() diff --git a/asv_bench/benchmarks/timeseries.py b/asv_bench/benchmarks/timeseries.py index ba0b51922fd31..6c9f8ee77e5ad 100644 --- a/asv_bench/benchmarks/timeseries.py +++ b/asv_bench/benchmarks/timeseries.py @@ -57,6 +57,9 @@ def time_to_date(self, index_type): def time_to_pydatetime(self, index_type): self.index.to_pydatetime() + def time_is_dates_only(self, index_type): + self.index._is_dates_only + class TzLocalize: @@ -91,20 +94,6 @@ def time_reest_datetimeindex(self, tz): self.df.reset_index() -class Factorize: - - params = [None, "Asia/Tokyo"] - param_names = "tz" - - def setup(self, tz): - N = 100000 - self.dti = date_range("2011-01-01", freq="H", periods=N, tz=tz) - self.dti = self.dti.repeat(5) - - def time_factorize(self, tz): - self.dti.factorize() - - class InferFreq: params = [None, "D", "B"] @@ -262,18 +251,6 @@ def time_get_slice(self, monotonic): self.s[:10000] -class IrregularOps: - def setup(self): - N = 10 ** 5 - idx = date_range(start="1/1/2000", periods=N, freq="s") - s = Series(np.random.randn(N), index=idx) - self.left = s.sample(frac=1) - self.right = s.sample(frac=1) - - def time_add(self): - self.left + self.right - - class Lookup: def setup(self): N = 1500000 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d992c64073476..42a039af46e94 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,78 +15,3 @@ jobs: parameters: name: Windows vmImage: vs2017-win2016 - -- job: 'Web_and_Docs' - pool: - vmImage: ubuntu-16.04 - timeoutInMinutes: 90 - steps: - - script: | - echo '##vso[task.setvariable variable=ENV_FILE]environment.yml' - echo '##vso[task.prependpath]$(HOME)/miniconda3/bin' - displayName: 'Setting environment variables' - - - script: | - sudo apt-get install -y libc6-dev-i386 - ci/setup_env.sh - displayName: 'Setup environment and build pandas' - - - script: | - source activate pandas-dev - python web/pandas_web.py web/pandas --target-path=web/build - displayName: 'Build website' - - - script: | - source activate pandas-dev - # Next we should simply have `doc/make.py --warnings-are-errors`, everything else is required because the ipython directive doesn't fail the build on errors (https://github.com/ipython/ipython/issues/11547) - doc/make.py --warnings-are-errors | tee sphinx.log ; SPHINX_RET=${PIPESTATUS[0]} - grep -B1 "^<<<-------------------------------------------------------------------------$" sphinx.log ; IPY_RET=$(( $? != 1 )) - exit $(( $SPHINX_RET + $IPY_RET )) - displayName: 'Build documentation' - - - script: | - mkdir -p to_deploy/docs - cp -r web/build/* to_deploy/ - cp -r doc/build/html/* to_deploy/docs/ - displayName: 'Merge website and docs' - - - script: | - cd to_deploy - git init - touch .nojekyll - echo "dev.pandas.io" > CNAME - printf "User-agent: *\nDisallow: /" > robots.txt - git add --all . - git config user.email "pandas-dev@python.org" - git config user.name "pandas-bot" - git commit -m "pandas web and documentation in master" - displayName: 'Create git repo for docs build' - condition : | - and(not(eq(variables['Build.Reason'], 'PullRequest')), - eq(variables['Build.SourceBranch'], 'refs/heads/master')) - - # For `InstallSSHKey@0` to work, next steps are required: - # 1. Generate a pair of private/public keys (i.e. `ssh-keygen -t rsa -b 4096 -C "your_email@example.com"`) - # 2. Go to "Library > Secure files" in the Azure Pipelines dashboard: https://dev.azure.com/pandas-dev/pandas/_library?itemType=SecureFiles - # 3. Click on "+ Secure file" - # 4. Upload the private key (the name of the file must match with the specified in "sshKeySecureFile" input below, "pandas_docs_key") - # 5. Click on file name after it is created, tick the box "Authorize for use in all pipelines" and save - # 6. The public key specified in "sshPublicKey" is the pair of the uploaded private key, and needs to be set as a deploy key of the repo where the docs will be pushed (with write access): https://github.com/pandas-dev/pandas-dev.github.io/settings/keys - - task: InstallSSHKey@0 - inputs: - hostName: 'github.com,192.30.252.128 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' - sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDHmz3l/EdqrgNxEUKkwDUuUcLv91unig03pYFGO/DMIgCmPdMG96zAgfnESd837Rm0wSSqylwSzkRJt5MV/TpFlcVifDLDQmUhqCeO8Z6dLl/oe35UKmyYICVwcvQTAaHNnYRpKC5IUlTh0JEtw9fGlnp1Ta7U1ENBLbKdpywczElhZu+hOQ892zqOj3CwA+U2329/d6cd7YnqIKoFN9DWT3kS5K6JE4IoBfQEVekIOs23bKjNLvPoOmi6CroAhu/K8j+NCWQjge5eJf2x/yTnIIP1PlEcXoHIr8io517posIx3TBup+CN8bNS1PpDW3jyD3ttl1uoBudjOQrobNnJeR6Rn67DRkG6IhSwr3BWj8alwUG5mTdZzwV5Pa9KZFdIiqX7NoDGg+itsR39QCn0thK8lGRNSR8KrWC1PSjecwelKBO7uQ7rnk/rkrZdBWR4oEA8YgNH8tirUw5WfOr5a0AIaJicKxGKNdMxZt+zmC+bS7F4YCOGIm9KHa43RrKhoGRhRf9fHHHKUPwFGqtWG4ykcUgoamDOURJyepesBAO3FiRE9rLU6ILbB3yEqqoekborHmAJD5vf7PWItW3Q/YQKuk3kkqRcKnexPyzyyq5lUgTi8CxxZdaASIOu294wjBhhdyHlXEkVTNJ9JKkj/obF+XiIIp0cBDsOXY9hDQ== pandas-dev@python.org' - sshKeySecureFile: 'pandas_docs_key' - displayName: 'Install GitHub ssh deployment key' - condition : | - and(not(eq(variables['Build.Reason'], 'PullRequest')), - eq(variables['Build.SourceBranch'], 'refs/heads/master')) - - - script: | - cd to_deploy - git remote add origin git@github.com:pandas-dev/pandas-dev.github.io.git - git push -f origin master - displayName: 'Publish web and docs to GitHub pages' - condition : | - and(not(eq(variables['Build.Reason'], 'PullRequest')), - eq(variables['Build.SourceBranch'], 'refs/heads/master')) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index b46989894ae12..e2dc543360a62 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -65,12 +65,12 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then flake8 --format="$FLAKE8_FORMAT" . RET=$(($RET + $?)) ; echo $MSG "DONE" - MSG='Linting .pyx code' ; echo $MSG - flake8 --format="$FLAKE8_FORMAT" pandas --filename=*.pyx --select=E501,E302,E203,E111,E114,E221,E303,E128,E231,E126,E265,E305,E301,E127,E261,E271,E129,W291,E222,E241,E123,F403,C400,C401,C402,C403,C404,C405,C406,C407,C408,C409,C410,C411 + MSG='Linting .pyx and .pxd code' ; echo $MSG + flake8 --format="$FLAKE8_FORMAT" pandas --append-config=flake8/cython.cfg RET=$(($RET + $?)) ; echo $MSG "DONE" - MSG='Linting .pxd and .pxi.in' ; echo $MSG - flake8 --format="$FLAKE8_FORMAT" pandas/_libs --filename=*.pxi.in,*.pxd --select=E501,E302,E203,E111,E114,E221,E303,E231,E126,F403 + MSG='Linting .pxi.in' ; echo $MSG + flake8 --format="$FLAKE8_FORMAT" pandas/_libs --append-config=flake8/cython-template.cfg RET=$(($RET + $?)) ; echo $MSG "DONE" echo "flake8-rst --version" @@ -259,8 +259,7 @@ fi if [[ -z "$CHECK" || "$CHECK" == "doctests" ]]; then MSG='Doctests frame.py' ; echo $MSG - pytest -q --doctest-modules pandas/core/frame.py \ - -k" -itertuples -join -reindex -reindex_axis -round" + pytest -q --doctest-modules pandas/core/frame.py RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Doctests series.py' ; echo $MSG @@ -270,7 +269,7 @@ if [[ -z "$CHECK" || "$CHECK" == "doctests" ]]; then MSG='Doctests generic.py' ; echo $MSG pytest -q --doctest-modules pandas/core/generic.py \ - -k"-_set_axis_name -_xs -describe -droplevel -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -to_json -transpose -values -xs -to_clipboard" + -k"-_set_axis_name -_xs -describe -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -to_json -transpose -values -xs -to_clipboard" RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Doctests groupby.py' ; echo $MSG @@ -294,8 +293,7 @@ if [[ -z "$CHECK" || "$CHECK" == "doctests" ]]; then MSG='Doctests interval classes' ; echo $MSG pytest -q --doctest-modules \ pandas/core/indexes/interval.py \ - pandas/core/arrays/interval.py \ - -k"-from_arrays -from_breaks -from_intervals -from_tuples -set_closed -to_tuples -interval_range" + pandas/core/arrays/interval.py RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Doctests arrays'; echo $MSG @@ -305,6 +303,10 @@ if [[ -z "$CHECK" || "$CHECK" == "doctests" ]]; then pandas/core/arrays/boolean.py RET=$(($RET + $?)) ; echo $MSG "DONE" + MSG='Doctests dtypes'; echo $MSG + pytest -q --doctest-modules pandas/core/dtypes/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + MSG='Doctests arrays/boolean.py' ; echo $MSG pytest -q --doctest-modules pandas/core/arrays/boolean.py RET=$(($RET + $?)) ; echo $MSG "DONE" diff --git a/ci/setup_env.sh b/ci/setup_env.sh index e5bee09fe2f79..ae39b0dda5d09 100755 --- a/ci/setup_env.sh +++ b/ci/setup_env.sh @@ -50,7 +50,7 @@ echo echo "update conda" conda config --set ssl_verify false conda config --set quiet true --set always_yes true --set changeps1 false -conda install pip # create conda to create a historical artifact for pip & setuptools +conda install pip conda # create conda to create a historical artifact for pip & setuptools conda update -n base conda echo "conda info -a" diff --git a/doc/data/air_quality_long.csv b/doc/data/air_quality_long.csv new file mode 100644 index 0000000000000..6225d65d8e276 --- /dev/null +++ b/doc/data/air_quality_long.csv @@ -0,0 +1,5273 @@ +city,country,date.utc,location,parameter,value,unit +Antwerpen,BE,2019-06-18 06:00:00+00:00,BETR801,pm25,18.0,µg/m³ +Antwerpen,BE,2019-06-17 08:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-06-17 07:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-06-17 06:00:00+00:00,BETR801,pm25,16.0,µg/m³ +Antwerpen,BE,2019-06-17 05:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-06-17 04:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-06-17 03:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-06-17 02:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-06-17 01:00:00+00:00,BETR801,pm25,8.0,µg/m³ +Antwerpen,BE,2019-06-16 01:00:00+00:00,BETR801,pm25,15.0,µg/m³ +Antwerpen,BE,2019-06-15 01:00:00+00:00,BETR801,pm25,11.0,µg/m³ +Antwerpen,BE,2019-06-14 09:00:00+00:00,BETR801,pm25,12.0,µg/m³ +Antwerpen,BE,2019-06-13 01:00:00+00:00,BETR801,pm25,3.0,µg/m³ +Antwerpen,BE,2019-06-12 01:00:00+00:00,BETR801,pm25,16.0,µg/m³ +Antwerpen,BE,2019-06-11 01:00:00+00:00,BETR801,pm25,3.5,µg/m³ +Antwerpen,BE,2019-06-10 01:00:00+00:00,BETR801,pm25,8.5,µg/m³ +Antwerpen,BE,2019-06-09 01:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-06-08 01:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-06-06 01:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-06-05 01:00:00+00:00,BETR801,pm25,11.0,µg/m³ +Antwerpen,BE,2019-06-04 01:00:00+00:00,BETR801,pm25,10.5,µg/m³ +Antwerpen,BE,2019-06-03 01:00:00+00:00,BETR801,pm25,12.5,µg/m³ +Antwerpen,BE,2019-06-02 01:00:00+00:00,BETR801,pm25,19.0,µg/m³ +Antwerpen,BE,2019-06-01 01:00:00+00:00,BETR801,pm25,9.0,µg/m³ +Antwerpen,BE,2019-05-31 01:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-05-30 01:00:00+00:00,BETR801,pm25,5.0,µg/m³ +Antwerpen,BE,2019-05-29 01:00:00+00:00,BETR801,pm25,5.5,µg/m³ +Antwerpen,BE,2019-05-28 01:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-05-27 01:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-05-26 01:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-05-25 01:00:00+00:00,BETR801,pm25,10.0,µg/m³ +Antwerpen,BE,2019-05-24 01:00:00+00:00,BETR801,pm25,13.0,µg/m³ +Antwerpen,BE,2019-05-23 01:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-05-22 01:00:00+00:00,BETR801,pm25,15.5,µg/m³ +Antwerpen,BE,2019-05-21 01:00:00+00:00,BETR801,pm25,20.5,µg/m³ +Antwerpen,BE,2019-05-20 17:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-05-20 16:00:00+00:00,BETR801,pm25,17.0,µg/m³ +Antwerpen,BE,2019-05-20 15:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-05-20 14:00:00+00:00,BETR801,pm25,14.5,µg/m³ +Antwerpen,BE,2019-05-20 13:00:00+00:00,BETR801,pm25,17.0,µg/m³ +Antwerpen,BE,2019-05-20 12:00:00+00:00,BETR801,pm25,17.5,µg/m³ +Antwerpen,BE,2019-05-20 11:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-20 10:00:00+00:00,BETR801,pm25,10.5,µg/m³ +Antwerpen,BE,2019-05-20 09:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-20 08:00:00+00:00,BETR801,pm25,19.5,µg/m³ +Antwerpen,BE,2019-05-20 07:00:00+00:00,BETR801,pm25,23.5,µg/m³ +Antwerpen,BE,2019-05-20 06:00:00+00:00,BETR801,pm25,22.0,µg/m³ +Antwerpen,BE,2019-05-20 05:00:00+00:00,BETR801,pm25,25.0,µg/m³ +Antwerpen,BE,2019-05-20 04:00:00+00:00,BETR801,pm25,24.5,µg/m³ +Antwerpen,BE,2019-05-20 03:00:00+00:00,BETR801,pm25,15.0,µg/m³ +Antwerpen,BE,2019-05-20 02:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-05-20 01:00:00+00:00,BETR801,pm25,28.0,µg/m³ +Antwerpen,BE,2019-05-19 21:00:00+00:00,BETR801,pm25,35.5,µg/m³ +Antwerpen,BE,2019-05-19 20:00:00+00:00,BETR801,pm25,40.0,µg/m³ +Antwerpen,BE,2019-05-19 19:00:00+00:00,BETR801,pm25,43.5,µg/m³ +Antwerpen,BE,2019-05-19 18:00:00+00:00,BETR801,pm25,35.0,µg/m³ +Antwerpen,BE,2019-05-19 17:00:00+00:00,BETR801,pm25,34.0,µg/m³ +Antwerpen,BE,2019-05-19 16:00:00+00:00,BETR801,pm25,36.5,µg/m³ +Antwerpen,BE,2019-05-19 15:00:00+00:00,BETR801,pm25,44.0,µg/m³ +Antwerpen,BE,2019-05-19 14:00:00+00:00,BETR801,pm25,43.5,µg/m³ +Antwerpen,BE,2019-05-19 13:00:00+00:00,BETR801,pm25,46.0,µg/m³ +Antwerpen,BE,2019-05-19 12:00:00+00:00,BETR801,pm25,43.0,µg/m³ +Antwerpen,BE,2019-05-19 11:00:00+00:00,BETR801,pm25,41.0,µg/m³ +Antwerpen,BE,2019-05-19 10:00:00+00:00,BETR801,pm25,41.5,µg/m³ +Antwerpen,BE,2019-05-19 09:00:00+00:00,BETR801,pm25,42.5,µg/m³ +Antwerpen,BE,2019-05-19 08:00:00+00:00,BETR801,pm25,51.5,µg/m³ +Antwerpen,BE,2019-05-19 07:00:00+00:00,BETR801,pm25,56.0,µg/m³ +Antwerpen,BE,2019-05-19 06:00:00+00:00,BETR801,pm25,58.5,µg/m³ +Antwerpen,BE,2019-05-19 05:00:00+00:00,BETR801,pm25,60.0,µg/m³ +Antwerpen,BE,2019-05-19 04:00:00+00:00,BETR801,pm25,56.5,µg/m³ +Antwerpen,BE,2019-05-19 03:00:00+00:00,BETR801,pm25,52.5,µg/m³ +Antwerpen,BE,2019-05-19 02:00:00+00:00,BETR801,pm25,51.5,µg/m³ +Antwerpen,BE,2019-05-19 01:00:00+00:00,BETR801,pm25,52.0,µg/m³ +Antwerpen,BE,2019-05-19 00:00:00+00:00,BETR801,pm25,49.5,µg/m³ +Antwerpen,BE,2019-05-18 23:00:00+00:00,BETR801,pm25,45.5,µg/m³ +Antwerpen,BE,2019-05-18 22:00:00+00:00,BETR801,pm25,42.0,µg/m³ +Antwerpen,BE,2019-05-18 21:00:00+00:00,BETR801,pm25,40.5,µg/m³ +Antwerpen,BE,2019-05-18 20:00:00+00:00,BETR801,pm25,41.0,µg/m³ +Antwerpen,BE,2019-05-18 19:00:00+00:00,BETR801,pm25,36.5,µg/m³ +Antwerpen,BE,2019-05-18 18:00:00+00:00,BETR801,pm25,37.0,µg/m³ +Antwerpen,BE,2019-05-18 01:00:00+00:00,BETR801,pm25,24.0,µg/m³ +Antwerpen,BE,2019-05-17 01:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-16 01:00:00+00:00,BETR801,pm25,11.0,µg/m³ +Antwerpen,BE,2019-05-15 02:00:00+00:00,BETR801,pm25,12.5,µg/m³ +Antwerpen,BE,2019-05-15 01:00:00+00:00,BETR801,pm25,13.0,µg/m³ +Antwerpen,BE,2019-05-14 02:00:00+00:00,BETR801,pm25,4.0,µg/m³ +Antwerpen,BE,2019-05-14 01:00:00+00:00,BETR801,pm25,4.0,µg/m³ +Antwerpen,BE,2019-05-13 02:00:00+00:00,BETR801,pm25,5.5,µg/m³ +Antwerpen,BE,2019-05-13 01:00:00+00:00,BETR801,pm25,5.0,µg/m³ +Antwerpen,BE,2019-05-12 02:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-05-12 01:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-05-11 02:00:00+00:00,BETR801,pm25,19.5,µg/m³ +Antwerpen,BE,2019-05-11 01:00:00+00:00,BETR801,pm25,17.0,µg/m³ +Antwerpen,BE,2019-05-10 02:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-10 01:00:00+00:00,BETR801,pm25,11.5,µg/m³ +Antwerpen,BE,2019-05-09 02:00:00+00:00,BETR801,pm25,3.5,µg/m³ +Antwerpen,BE,2019-05-09 01:00:00+00:00,BETR801,pm25,4.5,µg/m³ +Antwerpen,BE,2019-05-08 02:00:00+00:00,BETR801,pm25,14.0,µg/m³ +Antwerpen,BE,2019-05-08 01:00:00+00:00,BETR801,pm25,14.5,µg/m³ +Antwerpen,BE,2019-05-07 02:00:00+00:00,BETR801,pm25,14.0,µg/m³ +Antwerpen,BE,2019-05-07 01:00:00+00:00,BETR801,pm25,12.5,µg/m³ +Antwerpen,BE,2019-05-06 02:00:00+00:00,BETR801,pm25,10.5,µg/m³ +Antwerpen,BE,2019-05-06 01:00:00+00:00,BETR801,pm25,10.0,µg/m³ +Antwerpen,BE,2019-05-05 02:00:00+00:00,BETR801,pm25,3.0,µg/m³ +Antwerpen,BE,2019-05-05 01:00:00+00:00,BETR801,pm25,5.0,µg/m³ +Antwerpen,BE,2019-05-04 02:00:00+00:00,BETR801,pm25,4.5,µg/m³ +Antwerpen,BE,2019-05-04 01:00:00+00:00,BETR801,pm25,4.0,µg/m³ +Antwerpen,BE,2019-05-03 02:00:00+00:00,BETR801,pm25,9.5,µg/m³ +Antwerpen,BE,2019-05-03 01:00:00+00:00,BETR801,pm25,8.5,µg/m³ +Antwerpen,BE,2019-05-02 02:00:00+00:00,BETR801,pm25,45.5,µg/m³ +Antwerpen,BE,2019-05-02 01:00:00+00:00,BETR801,pm25,46.0,µg/m³ +Antwerpen,BE,2019-05-01 02:00:00+00:00,BETR801,pm25,28.5,µg/m³ +Antwerpen,BE,2019-05-01 01:00:00+00:00,BETR801,pm25,34.5,µg/m³ +Antwerpen,BE,2019-04-30 02:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-04-30 01:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-04-29 02:00:00+00:00,BETR801,pm25,14.5,µg/m³ +Antwerpen,BE,2019-04-29 01:00:00+00:00,BETR801,pm25,14.0,µg/m³ +Antwerpen,BE,2019-04-28 02:00:00+00:00,BETR801,pm25,4.5,µg/m³ +Antwerpen,BE,2019-04-28 01:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-04-27 02:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-04-27 01:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-04-26 02:00:00+00:00,BETR801,pm25,4.0,µg/m³ +Antwerpen,BE,2019-04-26 01:00:00+00:00,BETR801,pm25,4.5,µg/m³ +Antwerpen,BE,2019-04-25 02:00:00+00:00,BETR801,pm25,3.0,µg/m³ +Antwerpen,BE,2019-04-25 01:00:00+00:00,BETR801,pm25,3.0,µg/m³ +Antwerpen,BE,2019-04-24 02:00:00+00:00,BETR801,pm25,19.0,µg/m³ +Antwerpen,BE,2019-04-24 01:00:00+00:00,BETR801,pm25,19.0,µg/m³ +Antwerpen,BE,2019-04-23 02:00:00+00:00,BETR801,pm25,9.0,µg/m³ +Antwerpen,BE,2019-04-23 01:00:00+00:00,BETR801,pm25,9.0,µg/m³ +Antwerpen,BE,2019-04-22 02:00:00+00:00,BETR801,pm25,36.5,µg/m³ +Antwerpen,BE,2019-04-22 01:00:00+00:00,BETR801,pm25,32.5,µg/m³ +Antwerpen,BE,2019-04-21 02:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-04-21 01:00:00+00:00,BETR801,pm25,27.5,µg/m³ +Antwerpen,BE,2019-04-20 02:00:00+00:00,BETR801,pm25,20.0,µg/m³ +Antwerpen,BE,2019-04-20 01:00:00+00:00,BETR801,pm25,20.0,µg/m³ +Antwerpen,BE,2019-04-19 01:00:00+00:00,BETR801,pm25,20.0,µg/m³ +Antwerpen,BE,2019-04-18 02:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-04-18 01:00:00+00:00,BETR801,pm25,25.0,µg/m³ +Antwerpen,BE,2019-04-17 03:00:00+00:00,BETR801,pm25,9.0,µg/m³ +Antwerpen,BE,2019-04-17 02:00:00+00:00,BETR801,pm25,8.5,µg/m³ +Antwerpen,BE,2019-04-17 01:00:00+00:00,BETR801,pm25,8.0,µg/m³ +Antwerpen,BE,2019-04-16 02:00:00+00:00,BETR801,pm25,23.0,µg/m³ +Antwerpen,BE,2019-04-16 01:00:00+00:00,BETR801,pm25,24.0,µg/m³ +Antwerpen,BE,2019-04-15 15:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-04-15 14:00:00+00:00,BETR801,pm25,25.5,µg/m³ +Antwerpen,BE,2019-04-15 13:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-04-15 12:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-04-15 11:00:00+00:00,BETR801,pm25,26.0,µg/m³ +Antwerpen,BE,2019-04-15 10:00:00+00:00,BETR801,pm25,26.0,µg/m³ +Antwerpen,BE,2019-04-15 09:00:00+00:00,BETR801,pm25,21.5,µg/m³ +Antwerpen,BE,2019-04-15 08:00:00+00:00,BETR801,pm25,24.0,µg/m³ +Antwerpen,BE,2019-04-15 07:00:00+00:00,BETR801,pm25,24.0,µg/m³ +Antwerpen,BE,2019-04-15 06:00:00+00:00,BETR801,pm25,23.0,µg/m³ +Antwerpen,BE,2019-04-15 05:00:00+00:00,BETR801,pm25,23.0,µg/m³ +Antwerpen,BE,2019-04-15 04:00:00+00:00,BETR801,pm25,23.5,µg/m³ +Antwerpen,BE,2019-04-15 03:00:00+00:00,BETR801,pm25,24.5,µg/m³ +Antwerpen,BE,2019-04-15 02:00:00+00:00,BETR801,pm25,24.5,µg/m³ +Antwerpen,BE,2019-04-15 01:00:00+00:00,BETR801,pm25,25.5,µg/m³ +Antwerpen,BE,2019-04-12 02:00:00+00:00,BETR801,pm25,22.0,µg/m³ +Antwerpen,BE,2019-04-12 01:00:00+00:00,BETR801,pm25,22.0,µg/m³ +Antwerpen,BE,2019-04-11 02:00:00+00:00,BETR801,pm25,10.0,µg/m³ +Antwerpen,BE,2019-04-11 01:00:00+00:00,BETR801,pm25,9.0,µg/m³ +Antwerpen,BE,2019-04-10 02:00:00+00:00,BETR801,pm25,26.0,µg/m³ +Antwerpen,BE,2019-04-10 01:00:00+00:00,BETR801,pm25,24.5,µg/m³ +Antwerpen,BE,2019-04-09 13:00:00+00:00,BETR801,pm25,38.0,µg/m³ +Antwerpen,BE,2019-04-09 12:00:00+00:00,BETR801,pm25,41.5,µg/m³ +Antwerpen,BE,2019-04-09 11:00:00+00:00,BETR801,pm25,45.0,µg/m³ +Antwerpen,BE,2019-04-09 10:00:00+00:00,BETR801,pm25,44.5,µg/m³ +Antwerpen,BE,2019-04-09 09:00:00+00:00,BETR801,pm25,43.0,µg/m³ +Antwerpen,BE,2019-04-09 08:00:00+00:00,BETR801,pm25,44.0,µg/m³ +Antwerpen,BE,2019-04-09 07:00:00+00:00,BETR801,pm25,46.5,µg/m³ +Antwerpen,BE,2019-04-09 06:00:00+00:00,BETR801,pm25,52.5,µg/m³ +Antwerpen,BE,2019-04-09 05:00:00+00:00,BETR801,pm25,68.0,µg/m³ +Antwerpen,BE,2019-04-09 04:00:00+00:00,BETR801,pm25,83.5,µg/m³ +Antwerpen,BE,2019-04-09 03:00:00+00:00,BETR801,pm25,99.0,µg/m³ +Antwerpen,BE,2019-04-09 02:00:00+00:00,BETR801,pm25,91.5,µg/m³ +Antwerpen,BE,2019-04-09 01:00:00+00:00,BETR801,pm25,76.0,µg/m³ +London,GB,2019-06-21 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-20 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-20 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-20 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-19 13:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-19 12:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-19 11:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-19 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-06-18 23:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-06-18 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-06-18 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-18 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-15 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-14 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-14 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-13 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-13 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-13 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-08 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-08 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-08 03:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-08 02:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-08 00:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 23:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 21:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 20:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 19:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 18:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 17:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 16:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 15:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 14:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 13:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 12:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 11:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 10:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 09:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 08:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 07:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-06-07 06:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-06-07 05:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-06-07 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-07 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-07 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-07 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-07 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-06 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-06 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-05 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-05 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-05 01:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 23:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-01 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 22:00:00+00:00,London Westminster,pm25,5.0,µg/m³ +London,GB,2019-05-31 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-25 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-25 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-25 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-24 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-24 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-24 21:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 20:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 19:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 18:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-21 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-21 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-21 07:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-21 06:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-21 05:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-21 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-21 03:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-21 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-21 01:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-21 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 22:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 21:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 20:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 19:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 18:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 17:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-20 16:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-20 15:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 14:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 13:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 12:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 11:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 10:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-20 09:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 08:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-20 07:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-20 06:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-20 05:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-20 04:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 03:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 02:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 01:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 00:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 23:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 22:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 21:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 20:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 19:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 18:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 17:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 16:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 15:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 14:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 13:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 12:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 11:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 10:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 09:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 08:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 07:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 06:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 05:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 04:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 03:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 02:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 01:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 00:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 23:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 22:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 21:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 20:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 19:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 18:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 17:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 16:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 15:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 14:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 13:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 12:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 11:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 10:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 09:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-18 08:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-18 07:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-18 06:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-18 05:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-18 04:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-18 03:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-18 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-18 01:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-18 00:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-17 23:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-17 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 21:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 20:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 19:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 06:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 01:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-16 23:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-16 22:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-16 21:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-16 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 16:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 15:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 14:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 13:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 12:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 11:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 10:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 09:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 08:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 07:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 06:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 05:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 04:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 03:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 02:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 01:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-15 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-15 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-15 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-15 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-15 19:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 18:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 17:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 16:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 15:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 14:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 13:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 12:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-15 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-15 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-15 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 03:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 02:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-14 23:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-14 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-13 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-13 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-13 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-12 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-12 22:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 21:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 20:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 19:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 18:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-12 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-12 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-12 10:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 09:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 08:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 07:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-12 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-12 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-12 03:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-12 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-12 01:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-12 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-11 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-11 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 16:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 15:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-11 09:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 08:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 07:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-11 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-11 04:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-11 03:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-11 02:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-11 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-11 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-10 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-10 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 21:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 20:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 19:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 18:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 17:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 16:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 15:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 14:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 13:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 12:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 11:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 10:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-10 09:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 08:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 07:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 03:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 02:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 01:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-09 23:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-09 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-09 21:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-09 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 03:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 02:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 23:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 21:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 20:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 19:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 18:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 06:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 04:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-08 03:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-08 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 19:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-07 18:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-07 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-07 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-07 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-07 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-07 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-07 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-06 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-06 06:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-06 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-06 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-06 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-06 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-06 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-06 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-05 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-05 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-05 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-05 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-05 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-05 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-05 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-04 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-04 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-04 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-04 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-04 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-04 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-04 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-04 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-04 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-04 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-04 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-04 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-04 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-04 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-04 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-04 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-04 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-03 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-03 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-03 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-03 03:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-03 02:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-03 01:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-03 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-02 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-02 16:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-02 15:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-02 14:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-02 13:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-02 12:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-02 11:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-02 10:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 09:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 08:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 07:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 06:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 05:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 04:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 03:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 02:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 01:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-02 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 22:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 21:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 20:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 19:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-01 18:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-01 17:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-01 16:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 15:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 14:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 13:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 12:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 11:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 10:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 09:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 08:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 07:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 06:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-01 05:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 04:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-01 03:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-01 00:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-04-30 23:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-30 22:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 21:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 20:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 19:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 18:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 17:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 16:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 15:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 14:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 13:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 12:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 11:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 10:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 09:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-30 08:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-30 07:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-30 06:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-30 05:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-30 04:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-30 03:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-04-30 02:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-04-30 01:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-04-30 00:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-04-29 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-29 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-29 21:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-29 20:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-29 19:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-29 18:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 17:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 16:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 15:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 14:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 13:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 12:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 11:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 10:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-29 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-29 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-29 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-29 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-29 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-29 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-29 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-29 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-29 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-29 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-28 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-28 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-28 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-28 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-04-28 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-04-28 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-28 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-27 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-26 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-04-25 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-25 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-04-25 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-25 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-25 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-25 05:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-25 04:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-04-25 03:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-04-25 02:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-25 00:00:00+00:00,London Westminster,pm25,21.0,µg/m³ +London,GB,2019-04-24 23:00:00+00:00,London Westminster,pm25,22.0,µg/m³ +London,GB,2019-04-24 22:00:00+00:00,London Westminster,pm25,23.0,µg/m³ +London,GB,2019-04-24 21:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-24 20:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-24 19:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-24 18:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-24 17:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-24 16:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-24 15:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-24 14:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-24 13:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-24 12:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-24 11:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-24 10:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-24 09:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-24 08:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-24 07:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-24 06:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-24 05:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-24 04:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-24 03:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-24 02:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-24 00:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-23 23:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-23 22:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-23 21:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-23 20:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-23 19:00:00+00:00,London Westminster,pm25,32.0,µg/m³ +London,GB,2019-04-23 18:00:00+00:00,London Westminster,pm25,33.0,µg/m³ +London,GB,2019-04-23 17:00:00+00:00,London Westminster,pm25,33.0,µg/m³ +London,GB,2019-04-23 16:00:00+00:00,London Westminster,pm25,34.0,µg/m³ +London,GB,2019-04-23 15:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-23 14:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-23 13:00:00+00:00,London Westminster,pm25,34.0,µg/m³ +London,GB,2019-04-23 12:00:00+00:00,London Westminster,pm25,34.0,µg/m³ +London,GB,2019-04-23 11:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-23 10:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-23 09:00:00+00:00,London Westminster,pm25,36.0,µg/m³ +London,GB,2019-04-23 08:00:00+00:00,London Westminster,pm25,37.0,µg/m³ +London,GB,2019-04-23 07:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-23 06:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-23 05:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-23 04:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-23 03:00:00+00:00,London Westminster,pm25,44.0,µg/m³ +London,GB,2019-04-23 02:00:00+00:00,London Westminster,pm25,45.0,µg/m³ +London,GB,2019-04-23 01:00:00+00:00,London Westminster,pm25,45.0,µg/m³ +London,GB,2019-04-23 00:00:00+00:00,London Westminster,pm25,45.0,µg/m³ +London,GB,2019-04-22 23:00:00+00:00,London Westminster,pm25,44.0,µg/m³ +London,GB,2019-04-22 22:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-22 21:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-22 20:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-22 19:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-22 18:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-22 17:00:00+00:00,London Westminster,pm25,39.0,µg/m³ +London,GB,2019-04-22 16:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 15:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 14:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 13:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 12:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 11:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 10:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-22 09:00:00+00:00,London Westminster,pm25,37.0,µg/m³ +London,GB,2019-04-22 08:00:00+00:00,London Westminster,pm25,37.0,µg/m³ +London,GB,2019-04-22 07:00:00+00:00,London Westminster,pm25,36.0,µg/m³ +London,GB,2019-04-22 06:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-22 05:00:00+00:00,London Westminster,pm25,33.0,µg/m³ +London,GB,2019-04-22 04:00:00+00:00,London Westminster,pm25,32.0,µg/m³ +London,GB,2019-04-22 03:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-22 02:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-22 01:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-22 00:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-21 23:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-21 22:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-21 21:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 20:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 19:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 18:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-21 17:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 16:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 15:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 14:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-21 13:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 12:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 11:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-21 10:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-21 09:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-21 08:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-21 07:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-21 06:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-21 05:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 04:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 03:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 02:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 01:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-21 00:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-20 23:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-20 22:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-20 21:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-20 20:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-20 19:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-20 18:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-20 17:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-20 16:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 15:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 14:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 13:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 12:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 11:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 10:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 09:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-20 08:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-20 07:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-20 06:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-20 05:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-20 04:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 03:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 02:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 01:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-20 00:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-19 23:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-19 22:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-19 21:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-19 20:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-19 19:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-19 18:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-19 17:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-19 16:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-19 15:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-19 14:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-19 13:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-19 12:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-19 11:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-19 10:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-19 09:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-19 08:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-19 07:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-19 06:00:00+00:00,London Westminster,pm25,31.0,µg/m³ +London,GB,2019-04-19 05:00:00+00:00,London Westminster,pm25,32.0,µg/m³ +London,GB,2019-04-19 04:00:00+00:00,London Westminster,pm25,34.0,µg/m³ +London,GB,2019-04-19 03:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-19 02:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-19 00:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-18 23:00:00+00:00,London Westminster,pm25,45.0,µg/m³ +London,GB,2019-04-18 22:00:00+00:00,London Westminster,pm25,47.0,µg/m³ +London,GB,2019-04-18 21:00:00+00:00,London Westminster,pm25,49.0,µg/m³ +London,GB,2019-04-18 20:00:00+00:00,London Westminster,pm25,50.0,µg/m³ +London,GB,2019-04-18 19:00:00+00:00,London Westminster,pm25,51.0,µg/m³ +London,GB,2019-04-18 18:00:00+00:00,London Westminster,pm25,51.0,µg/m³ +London,GB,2019-04-18 17:00:00+00:00,London Westminster,pm25,51.0,µg/m³ +London,GB,2019-04-18 16:00:00+00:00,London Westminster,pm25,52.0,µg/m³ +London,GB,2019-04-18 15:00:00+00:00,London Westminster,pm25,53.0,µg/m³ +London,GB,2019-04-18 14:00:00+00:00,London Westminster,pm25,53.0,µg/m³ +London,GB,2019-04-18 13:00:00+00:00,London Westminster,pm25,53.0,µg/m³ +London,GB,2019-04-18 12:00:00+00:00,London Westminster,pm25,54.0,µg/m³ +London,GB,2019-04-18 11:00:00+00:00,London Westminster,pm25,55.0,µg/m³ +London,GB,2019-04-18 10:00:00+00:00,London Westminster,pm25,55.0,µg/m³ +London,GB,2019-04-18 09:00:00+00:00,London Westminster,pm25,55.0,µg/m³ +London,GB,2019-04-18 08:00:00+00:00,London Westminster,pm25,55.0,µg/m³ +London,GB,2019-04-18 07:00:00+00:00,London Westminster,pm25,55.0,µg/m³ +London,GB,2019-04-18 06:00:00+00:00,London Westminster,pm25,54.0,µg/m³ +London,GB,2019-04-18 05:00:00+00:00,London Westminster,pm25,53.0,µg/m³ +London,GB,2019-04-18 04:00:00+00:00,London Westminster,pm25,52.0,µg/m³ +London,GB,2019-04-18 03:00:00+00:00,London Westminster,pm25,50.0,µg/m³ +London,GB,2019-04-18 02:00:00+00:00,London Westminster,pm25,48.0,µg/m³ +London,GB,2019-04-18 01:00:00+00:00,London Westminster,pm25,46.0,µg/m³ +London,GB,2019-04-18 00:00:00+00:00,London Westminster,pm25,44.0,µg/m³ +London,GB,2019-04-17 23:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-17 22:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-17 21:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-17 20:00:00+00:00,London Westminster,pm25,39.0,µg/m³ +London,GB,2019-04-17 19:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 18:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 17:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 16:00:00+00:00,London Westminster,pm25,37.0,µg/m³ +London,GB,2019-04-17 15:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 14:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 13:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 12:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 11:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 10:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-17 09:00:00+00:00,London Westminster,pm25,39.0,µg/m³ +London,GB,2019-04-17 08:00:00+00:00,London Westminster,pm25,39.0,µg/m³ +London,GB,2019-04-17 07:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-17 06:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-17 05:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-17 04:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-17 03:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-17 02:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-17 00:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 23:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 22:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 21:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 20:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 19:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 18:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 17:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-16 15:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-16 14:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-16 13:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-16 12:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-16 11:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-16 10:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-16 09:00:00+00:00,London Westminster,pm25,37.0,µg/m³ +London,GB,2019-04-16 08:00:00+00:00,London Westminster,pm25,36.0,µg/m³ +London,GB,2019-04-16 07:00:00+00:00,London Westminster,pm25,36.0,µg/m³ +London,GB,2019-04-16 06:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-16 05:00:00+00:00,London Westminster,pm25,34.0,µg/m³ +London,GB,2019-04-16 04:00:00+00:00,London Westminster,pm25,32.0,µg/m³ +London,GB,2019-04-16 03:00:00+00:00,London Westminster,pm25,32.0,µg/m³ +London,GB,2019-04-16 02:00:00+00:00,London Westminster,pm25,31.0,µg/m³ +London,GB,2019-04-16 00:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-15 23:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-15 22:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-15 21:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-15 20:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-15 19:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-15 18:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-15 17:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-15 16:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-15 15:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-15 14:00:00+00:00,London Westminster,pm25,28.0,µg/m³ +London,GB,2019-04-15 13:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-15 12:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-15 11:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-15 10:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-15 09:00:00+00:00,London Westminster,pm25,25.0,µg/m³ +London,GB,2019-04-15 08:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-15 07:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-15 06:00:00+00:00,London Westminster,pm25,23.0,µg/m³ +London,GB,2019-04-15 05:00:00+00:00,London Westminster,pm25,22.0,µg/m³ +London,GB,2019-04-15 04:00:00+00:00,London Westminster,pm25,22.0,µg/m³ +London,GB,2019-04-15 03:00:00+00:00,London Westminster,pm25,21.0,µg/m³ +London,GB,2019-04-15 02:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-04-15 01:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-15 00:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-14 23:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-04-14 22:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-04-14 21:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-04-14 20:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-14 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-14 18:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 17:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 16:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 15:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 14:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 13:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 12:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 11:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 10:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 09:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-14 08:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 07:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 03:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-14 01:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-14 00:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-13 23:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 16:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 15:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 14:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 13:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 12:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 11:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 10:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 09:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-13 08:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 07:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 06:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 05:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 04:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 03:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 01:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-13 00:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 23:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 16:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 15:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 14:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 13:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 12:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-12 11:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 10:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 09:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 08:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 07:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-12 05:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-12 04:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-12 03:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-12 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-11 23:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-11 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-11 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-11 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-11 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-11 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-04-11 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 06:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-04-11 03:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-11 02:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-11 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-04-10 23:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-10 22:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-04-10 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-10 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-10 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-10 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-10 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-04-10 16:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-10 15:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-04-10 14:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-04-10 13:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-04-10 12:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-04-10 11:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-04-10 10:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-04-10 09:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-04-10 08:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-04-10 07:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-04-10 06:00:00+00:00,London Westminster,pm25,21.0,µg/m³ +London,GB,2019-04-10 05:00:00+00:00,London Westminster,pm25,22.0,µg/m³ +London,GB,2019-04-10 04:00:00+00:00,London Westminster,pm25,24.0,µg/m³ +London,GB,2019-04-10 03:00:00+00:00,London Westminster,pm25,26.0,µg/m³ +London,GB,2019-04-10 02:00:00+00:00,London Westminster,pm25,27.0,µg/m³ +London,GB,2019-04-10 01:00:00+00:00,London Westminster,pm25,29.0,µg/m³ +London,GB,2019-04-10 00:00:00+00:00,London Westminster,pm25,30.0,µg/m³ +London,GB,2019-04-09 23:00:00+00:00,London Westminster,pm25,32.0,µg/m³ +London,GB,2019-04-09 22:00:00+00:00,London Westminster,pm25,34.0,µg/m³ +London,GB,2019-04-09 21:00:00+00:00,London Westminster,pm25,35.0,µg/m³ +London,GB,2019-04-09 20:00:00+00:00,London Westminster,pm25,36.0,µg/m³ +London,GB,2019-04-09 19:00:00+00:00,London Westminster,pm25,37.0,µg/m³ +London,GB,2019-04-09 18:00:00+00:00,London Westminster,pm25,38.0,µg/m³ +London,GB,2019-04-09 17:00:00+00:00,London Westminster,pm25,39.0,µg/m³ +London,GB,2019-04-09 16:00:00+00:00,London Westminster,pm25,39.0,µg/m³ +London,GB,2019-04-09 15:00:00+00:00,London Westminster,pm25,40.0,µg/m³ +London,GB,2019-04-09 14:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-09 13:00:00+00:00,London Westminster,pm25,41.0,µg/m³ +London,GB,2019-04-09 12:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-09 11:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-09 10:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-09 09:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-09 08:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-09 07:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-09 06:00:00+00:00,London Westminster,pm25,44.0,µg/m³ +London,GB,2019-04-09 05:00:00+00:00,London Westminster,pm25,44.0,µg/m³ +London,GB,2019-04-09 04:00:00+00:00,London Westminster,pm25,43.0,µg/m³ +London,GB,2019-04-09 03:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +London,GB,2019-04-09 02:00:00+00:00,London Westminster,pm25,42.0,µg/m³ +Paris,FR,2019-06-21 00:00:00+00:00,FR04014,no2,20.0,µg/m³ +Paris,FR,2019-06-20 23:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-06-20 22:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-06-20 21:00:00+00:00,FR04014,no2,24.9,µg/m³ +Paris,FR,2019-06-20 20:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-06-20 19:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-06-20 18:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-06-20 17:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-06-20 16:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-06-20 15:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-06-20 14:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-06-20 13:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-06-19 10:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-06-19 09:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-06-18 22:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-06-18 21:00:00+00:00,FR04014,no2,23.1,µg/m³ +Paris,FR,2019-06-18 20:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-06-18 19:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-06-18 08:00:00+00:00,FR04014,no2,49.6,µg/m³ +Paris,FR,2019-06-18 07:00:00+00:00,FR04014,no2,52.6,µg/m³ +Paris,FR,2019-06-18 06:00:00+00:00,FR04014,no2,51.4,µg/m³ +Paris,FR,2019-06-18 05:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-06-18 04:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-06-18 03:00:00+00:00,FR04014,no2,45.5,µg/m³ +Paris,FR,2019-06-18 02:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-06-18 01:00:00+00:00,FR04014,no2,60.1,µg/m³ +Paris,FR,2019-06-18 00:00:00+00:00,FR04014,no2,66.2,µg/m³ +Paris,FR,2019-06-17 23:00:00+00:00,FR04014,no2,73.3,µg/m³ +Paris,FR,2019-06-17 22:00:00+00:00,FR04014,no2,51.0,µg/m³ +Paris,FR,2019-06-17 21:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-06-17 20:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-06-17 19:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-17 18:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-06-17 17:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-06-17 16:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-06-17 15:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-17 14:00:00+00:00,FR04014,no2,11.5,µg/m³ +Paris,FR,2019-06-17 13:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-17 12:00:00+00:00,FR04014,no2,10.1,µg/m³ +Paris,FR,2019-06-17 11:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-17 10:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-06-17 09:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-06-17 08:00:00+00:00,FR04014,no2,51.6,µg/m³ +Paris,FR,2019-06-17 07:00:00+00:00,FR04014,no2,54.4,µg/m³ +Paris,FR,2019-06-17 06:00:00+00:00,FR04014,no2,52.3,µg/m³ +Paris,FR,2019-06-17 05:00:00+00:00,FR04014,no2,44.8,µg/m³ +Paris,FR,2019-06-17 04:00:00+00:00,FR04014,no2,45.7,µg/m³ +Paris,FR,2019-06-17 03:00:00+00:00,FR04014,no2,49.1,µg/m³ +Paris,FR,2019-06-17 02:00:00+00:00,FR04014,no2,53.1,µg/m³ +Paris,FR,2019-06-17 01:00:00+00:00,FR04014,no2,58.8,µg/m³ +Paris,FR,2019-06-17 00:00:00+00:00,FR04014,no2,69.3,µg/m³ +Paris,FR,2019-06-16 23:00:00+00:00,FR04014,no2,67.3,µg/m³ +Paris,FR,2019-06-16 22:00:00+00:00,FR04014,no2,56.6,µg/m³ +Paris,FR,2019-06-16 21:00:00+00:00,FR04014,no2,42.7,µg/m³ +Paris,FR,2019-06-16 20:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-06-16 19:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-16 18:00:00+00:00,FR04014,no2,12.3,µg/m³ +Paris,FR,2019-06-16 17:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-16 16:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-06-16 15:00:00+00:00,FR04014,no2,8.4,µg/m³ +Paris,FR,2019-06-16 14:00:00+00:00,FR04014,no2,8.1,µg/m³ +Paris,FR,2019-06-16 13:00:00+00:00,FR04014,no2,8.7,µg/m³ +Paris,FR,2019-06-16 12:00:00+00:00,FR04014,no2,11.2,µg/m³ +Paris,FR,2019-06-16 11:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-06-16 10:00:00+00:00,FR04014,no2,8.7,µg/m³ +Paris,FR,2019-06-16 09:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-06-16 08:00:00+00:00,FR04014,no2,9.9,µg/m³ +Paris,FR,2019-06-16 07:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-16 06:00:00+00:00,FR04014,no2,11.6,µg/m³ +Paris,FR,2019-06-16 05:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-16 04:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-16 03:00:00+00:00,FR04014,no2,11.2,µg/m³ +Paris,FR,2019-06-16 02:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-06-16 01:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-06-16 00:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-06-15 23:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-06-15 22:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-06-15 21:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-06-15 20:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-15 19:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-06-15 18:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-15 17:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-06-15 16:00:00+00:00,FR04014,no2,10.7,µg/m³ +Paris,FR,2019-06-15 15:00:00+00:00,FR04014,no2,10.5,µg/m³ +Paris,FR,2019-06-15 14:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-15 13:00:00+00:00,FR04014,no2,9.0,µg/m³ +Paris,FR,2019-06-15 12:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-06-15 11:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-06-15 10:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-06-15 09:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-15 08:00:00+00:00,FR04014,no2,17.6,µg/m³ +Paris,FR,2019-06-15 07:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-06-15 06:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-06-15 02:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-06-15 01:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-06-15 00:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-06-14 23:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-06-14 22:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-06-14 21:00:00+00:00,FR04014,no2,55.0,µg/m³ +Paris,FR,2019-06-14 20:00:00+00:00,FR04014,no2,41.9,µg/m³ +Paris,FR,2019-06-14 19:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-06-14 18:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-06-14 17:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-06-14 16:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-06-14 15:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-14 14:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-06-14 13:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-06-14 12:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-06-14 11:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-06-14 10:00:00+00:00,FR04014,no2,25.1,µg/m³ +Paris,FR,2019-06-14 09:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-06-14 08:00:00+00:00,FR04014,no2,34.3,µg/m³ +Paris,FR,2019-06-14 07:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-06-14 06:00:00+00:00,FR04014,no2,64.3,µg/m³ +Paris,FR,2019-06-14 05:00:00+00:00,FR04014,no2,49.3,µg/m³ +Paris,FR,2019-06-14 04:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-06-14 03:00:00+00:00,FR04014,no2,48.5,µg/m³ +Paris,FR,2019-06-14 02:00:00+00:00,FR04014,no2,66.6,µg/m³ +Paris,FR,2019-06-14 01:00:00+00:00,FR04014,no2,68.1,µg/m³ +Paris,FR,2019-06-14 00:00:00+00:00,FR04014,no2,74.2,µg/m³ +Paris,FR,2019-06-13 23:00:00+00:00,FR04014,no2,78.3,µg/m³ +Paris,FR,2019-06-13 22:00:00+00:00,FR04014,no2,77.9,µg/m³ +Paris,FR,2019-06-13 21:00:00+00:00,FR04014,no2,58.8,µg/m³ +Paris,FR,2019-06-13 20:00:00+00:00,FR04014,no2,31.5,µg/m³ +Paris,FR,2019-06-13 19:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-06-13 18:00:00+00:00,FR04014,no2,24.0,µg/m³ +Paris,FR,2019-06-13 17:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-06-13 16:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-06-13 15:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-06-13 14:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-06-13 13:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-06-13 12:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-06-13 11:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-06-13 10:00:00+00:00,FR04014,no2,24.5,µg/m³ +Paris,FR,2019-06-13 09:00:00+00:00,FR04014,no2,30.2,µg/m³ +Paris,FR,2019-06-13 08:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-06-13 07:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-06-13 06:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-06-13 05:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-06-13 04:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-06-13 03:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-06-13 02:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-06-13 01:00:00+00:00,FR04014,no2,18.7,µg/m³ +Paris,FR,2019-06-13 00:00:00+00:00,FR04014,no2,20.0,µg/m³ +Paris,FR,2019-06-12 23:00:00+00:00,FR04014,no2,26.9,µg/m³ +Paris,FR,2019-06-12 22:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-06-12 21:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-06-12 20:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-06-12 19:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-06-12 18:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-06-12 17:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-06-12 16:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-06-12 15:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-06-12 14:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-06-12 13:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-06-12 12:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-06-12 11:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-06-12 10:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-06-12 09:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-06-12 08:00:00+00:00,FR04014,no2,35.5,µg/m³ +Paris,FR,2019-06-12 07:00:00+00:00,FR04014,no2,44.4,µg/m³ +Paris,FR,2019-06-12 06:00:00+00:00,FR04014,no2,38.4,µg/m³ +Paris,FR,2019-06-12 05:00:00+00:00,FR04014,no2,42.7,µg/m³ +Paris,FR,2019-06-12 04:00:00+00:00,FR04014,no2,44.9,µg/m³ +Paris,FR,2019-06-12 03:00:00+00:00,FR04014,no2,36.3,µg/m³ +Paris,FR,2019-06-12 02:00:00+00:00,FR04014,no2,34.7,µg/m³ +Paris,FR,2019-06-12 01:00:00+00:00,FR04014,no2,41.9,µg/m³ +Paris,FR,2019-06-12 00:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-06-11 23:00:00+00:00,FR04014,no2,41.5,µg/m³ +Paris,FR,2019-06-11 22:00:00+00:00,FR04014,no2,59.4,µg/m³ +Paris,FR,2019-06-11 21:00:00+00:00,FR04014,no2,54.1,µg/m³ +Paris,FR,2019-06-11 20:00:00+00:00,FR04014,no2,42.7,µg/m³ +Paris,FR,2019-06-11 19:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-06-11 18:00:00+00:00,FR04014,no2,44.6,µg/m³ +Paris,FR,2019-06-11 17:00:00+00:00,FR04014,no2,35.5,µg/m³ +Paris,FR,2019-06-11 16:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-06-11 15:00:00+00:00,FR04014,no2,19.8,µg/m³ +Paris,FR,2019-06-11 14:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-06-11 13:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-11 12:00:00+00:00,FR04014,no2,12.6,µg/m³ +Paris,FR,2019-06-11 11:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-06-11 10:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-06-11 09:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-06-11 08:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-06-11 07:00:00+00:00,FR04014,no2,58.0,µg/m³ +Paris,FR,2019-06-11 06:00:00+00:00,FR04014,no2,55.4,µg/m³ +Paris,FR,2019-06-11 05:00:00+00:00,FR04014,no2,58.7,µg/m³ +Paris,FR,2019-06-11 04:00:00+00:00,FR04014,no2,52.7,µg/m³ +Paris,FR,2019-06-11 03:00:00+00:00,FR04014,no2,32.3,µg/m³ +Paris,FR,2019-06-11 02:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-06-11 01:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-06-11 00:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-06-10 23:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-06-10 22:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-06-10 21:00:00+00:00,FR04014,no2,23.5,µg/m³ +Paris,FR,2019-06-10 20:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-06-10 19:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-06-10 18:00:00+00:00,FR04014,no2,18.4,µg/m³ +Paris,FR,2019-06-10 17:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-06-10 16:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-06-10 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-10 14:00:00+00:00,FR04014,no2,9.5,µg/m³ +Paris,FR,2019-06-10 13:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-10 12:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-10 11:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-06-10 10:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-06-10 09:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-06-10 08:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-06-10 07:00:00+00:00,FR04014,no2,23.0,µg/m³ +Paris,FR,2019-06-10 06:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-06-10 05:00:00+00:00,FR04014,no2,21.3,µg/m³ +Paris,FR,2019-06-10 04:00:00+00:00,FR04014,no2,13.7,µg/m³ +Paris,FR,2019-06-10 03:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-06-10 02:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-06-10 01:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-06-10 00:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-06-09 23:00:00+00:00,FR04014,no2,39.9,µg/m³ +Paris,FR,2019-06-09 22:00:00+00:00,FR04014,no2,37.1,µg/m³ +Paris,FR,2019-06-09 21:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-06-09 20:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-06-09 19:00:00+00:00,FR04014,no2,30.6,µg/m³ +Paris,FR,2019-06-09 18:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-09 17:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-06-09 16:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-06-09 15:00:00+00:00,FR04014,no2,7.2,µg/m³ +Paris,FR,2019-06-09 14:00:00+00:00,FR04014,no2,7.9,µg/m³ +Paris,FR,2019-06-09 13:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-09 12:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-06-09 11:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-06-09 10:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-06-09 09:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-06-09 08:00:00+00:00,FR04014,no2,30.2,µg/m³ +Paris,FR,2019-06-09 07:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-06-09 06:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-06-09 05:00:00+00:00,FR04014,no2,42.2,µg/m³ +Paris,FR,2019-06-09 04:00:00+00:00,FR04014,no2,43.0,µg/m³ +Paris,FR,2019-06-09 03:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-06-09 02:00:00+00:00,FR04014,no2,51.2,µg/m³ +Paris,FR,2019-06-09 01:00:00+00:00,FR04014,no2,41.0,µg/m³ +Paris,FR,2019-06-09 00:00:00+00:00,FR04014,no2,55.9,µg/m³ +Paris,FR,2019-06-08 23:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-06-08 22:00:00+00:00,FR04014,no2,34.8,µg/m³ +Paris,FR,2019-06-08 21:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-06-08 18:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-06-08 17:00:00+00:00,FR04014,no2,14.8,µg/m³ +Paris,FR,2019-06-08 16:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-06-08 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-08 14:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-06-08 13:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-06-08 12:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-06-08 11:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-06-08 10:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-06-08 09:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-08 08:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-06-08 07:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-08 06:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-06-08 05:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-06-08 04:00:00+00:00,FR04014,no2,10.7,µg/m³ +Paris,FR,2019-06-08 03:00:00+00:00,FR04014,no2,9.8,µg/m³ +Paris,FR,2019-06-08 02:00:00+00:00,FR04014,no2,8.4,µg/m³ +Paris,FR,2019-06-08 01:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-08 00:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-06-07 23:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-07 22:00:00+00:00,FR04014,no2,14.7,µg/m³ +Paris,FR,2019-06-07 21:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-06-07 20:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-06-07 19:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-06-07 18:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-06-07 17:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-07 16:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-07 15:00:00+00:00,FR04014,no2,15.6,µg/m³ +Paris,FR,2019-06-07 14:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-07 13:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-06-07 12:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-06-07 11:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-06-07 10:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-06-07 09:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-06-07 08:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-06-07 07:00:00+00:00,FR04014,no2,23.0,µg/m³ +Paris,FR,2019-06-07 06:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-06-06 14:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-06-06 13:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-06-06 12:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-06-06 11:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-06-06 10:00:00+00:00,FR04014,no2,21.2,µg/m³ +Paris,FR,2019-06-06 09:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-06-06 08:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-06-06 07:00:00+00:00,FR04014,no2,43.1,µg/m³ +Paris,FR,2019-06-06 06:00:00+00:00,FR04014,no2,40.5,µg/m³ +Paris,FR,2019-06-06 05:00:00+00:00,FR04014,no2,40.3,µg/m³ +Paris,FR,2019-06-06 04:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-06-06 03:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-06-06 02:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-06 01:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-06-06 00:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-06-05 23:00:00+00:00,FR04014,no2,31.8,µg/m³ +Paris,FR,2019-06-05 22:00:00+00:00,FR04014,no2,30.3,µg/m³ +Paris,FR,2019-06-05 21:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-06-05 20:00:00+00:00,FR04014,no2,37.5,µg/m³ +Paris,FR,2019-06-05 19:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-06-05 18:00:00+00:00,FR04014,no2,40.8,µg/m³ +Paris,FR,2019-06-05 17:00:00+00:00,FR04014,no2,48.8,µg/m³ +Paris,FR,2019-06-05 16:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-06-05 15:00:00+00:00,FR04014,no2,53.5,µg/m³ +Paris,FR,2019-06-05 14:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-06-05 13:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-06-05 12:00:00+00:00,FR04014,no2,47.2,µg/m³ +Paris,FR,2019-06-05 11:00:00+00:00,FR04014,no2,59.0,µg/m³ +Paris,FR,2019-06-05 10:00:00+00:00,FR04014,no2,42.1,µg/m³ +Paris,FR,2019-06-05 09:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-06-05 08:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-06-05 07:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-06-05 06:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-06-05 05:00:00+00:00,FR04014,no2,39.2,µg/m³ +Paris,FR,2019-06-05 04:00:00+00:00,FR04014,no2,24.5,µg/m³ +Paris,FR,2019-06-05 03:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-06-05 02:00:00+00:00,FR04014,no2,12.4,µg/m³ +Paris,FR,2019-06-05 01:00:00+00:00,FR04014,no2,10.8,µg/m³ +Paris,FR,2019-06-05 00:00:00+00:00,FR04014,no2,15.7,µg/m³ +Paris,FR,2019-06-04 23:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-06-04 22:00:00+00:00,FR04014,no2,33.5,µg/m³ +Paris,FR,2019-06-04 21:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-06-04 20:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-06-04 19:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-06-04 18:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-06-04 17:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-06-04 16:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-06-04 15:00:00+00:00,FR04014,no2,21.5,µg/m³ +Paris,FR,2019-06-04 14:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-06-04 13:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-06-04 12:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-06-04 11:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-06-04 10:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-06-04 09:00:00+00:00,FR04014,no2,38.5,µg/m³ +Paris,FR,2019-06-04 08:00:00+00:00,FR04014,no2,50.8,µg/m³ +Paris,FR,2019-06-04 07:00:00+00:00,FR04014,no2,53.5,µg/m³ +Paris,FR,2019-06-04 06:00:00+00:00,FR04014,no2,47.7,µg/m³ +Paris,FR,2019-06-04 05:00:00+00:00,FR04014,no2,36.5,µg/m³ +Paris,FR,2019-06-04 04:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-06-04 03:00:00+00:00,FR04014,no2,41.6,µg/m³ +Paris,FR,2019-06-04 02:00:00+00:00,FR04014,no2,35.0,µg/m³ +Paris,FR,2019-06-04 01:00:00+00:00,FR04014,no2,43.9,µg/m³ +Paris,FR,2019-06-04 00:00:00+00:00,FR04014,no2,52.4,µg/m³ +Paris,FR,2019-06-03 23:00:00+00:00,FR04014,no2,44.6,µg/m³ +Paris,FR,2019-06-03 22:00:00+00:00,FR04014,no2,30.5,µg/m³ +Paris,FR,2019-06-03 21:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-06-03 20:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-06-03 19:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-06-03 18:00:00+00:00,FR04014,no2,23.1,µg/m³ +Paris,FR,2019-06-03 17:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-06-03 16:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-06-03 15:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-06-03 14:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-06-03 13:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-06-03 12:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-06-03 11:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-06-03 10:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-06-03 09:00:00+00:00,FR04014,no2,46.0,µg/m³ +Paris,FR,2019-06-03 08:00:00+00:00,FR04014,no2,43.9,µg/m³ +Paris,FR,2019-06-03 07:00:00+00:00,FR04014,no2,50.0,µg/m³ +Paris,FR,2019-06-03 06:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-06-03 05:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-06-03 04:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-06-03 03:00:00+00:00,FR04014,no2,9.8,µg/m³ +Paris,FR,2019-06-03 02:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-03 01:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-03 00:00:00+00:00,FR04014,no2,15.7,µg/m³ +Paris,FR,2019-06-02 23:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-06-02 22:00:00+00:00,FR04014,no2,27.6,µg/m³ +Paris,FR,2019-06-02 21:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-06-02 20:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-06-02 19:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-06-02 18:00:00+00:00,FR04014,no2,15.6,µg/m³ +Paris,FR,2019-06-02 17:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-02 16:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-02 15:00:00+00:00,FR04014,no2,13.9,µg/m³ +Paris,FR,2019-06-02 14:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-06-02 13:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-06-02 12:00:00+00:00,FR04014,no2,11.5,µg/m³ +Paris,FR,2019-06-02 11:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-02 10:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-06-02 09:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-06-02 08:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-02 07:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-06-02 06:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-06-02 05:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-06-02 04:00:00+00:00,FR04014,no2,24.5,µg/m³ +Paris,FR,2019-06-02 03:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-06-02 02:00:00+00:00,FR04014,no2,39.2,µg/m³ +Paris,FR,2019-06-02 01:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-06-02 00:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-06-01 23:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-06-01 22:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-06-01 21:00:00+00:00,FR04014,no2,49.4,µg/m³ +Paris,FR,2019-06-01 20:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-06-01 19:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-06-01 18:00:00+00:00,FR04014,no2,14.5,µg/m³ +Paris,FR,2019-06-01 17:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-01 16:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-01 15:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-01 14:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-06-01 13:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-01 12:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-06-01 11:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-06-01 10:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-06-01 09:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-06-01 08:00:00+00:00,FR04014,no2,33.3,µg/m³ +Paris,FR,2019-06-01 07:00:00+00:00,FR04014,no2,46.4,µg/m³ +Paris,FR,2019-06-01 06:00:00+00:00,FR04014,no2,44.6,µg/m³ +Paris,FR,2019-06-01 02:00:00+00:00,FR04014,no2,68.1,µg/m³ +Paris,FR,2019-06-01 01:00:00+00:00,FR04014,no2,74.8,µg/m³ +Paris,FR,2019-06-01 00:00:00+00:00,FR04014,no2,84.7,µg/m³ +Paris,FR,2019-05-31 23:00:00+00:00,FR04014,no2,81.7,µg/m³ +Paris,FR,2019-05-31 22:00:00+00:00,FR04014,no2,68.0,µg/m³ +Paris,FR,2019-05-31 21:00:00+00:00,FR04014,no2,60.2,µg/m³ +Paris,FR,2019-05-31 20:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-31 19:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-31 18:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-31 17:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-31 16:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-05-31 15:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-31 14:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-31 13:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-05-31 12:00:00+00:00,FR04014,no2,13.3,µg/m³ +Paris,FR,2019-05-31 11:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-31 10:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-31 09:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-05-31 08:00:00+00:00,FR04014,no2,36.6,µg/m³ +Paris,FR,2019-05-31 07:00:00+00:00,FR04014,no2,47.4,µg/m³ +Paris,FR,2019-05-31 06:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-31 05:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-05-31 04:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-05-31 03:00:00+00:00,FR04014,no2,40.1,µg/m³ +Paris,FR,2019-05-31 02:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-05-31 01:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-05-31 00:00:00+00:00,FR04014,no2,27.2,µg/m³ +Paris,FR,2019-05-30 23:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-05-30 22:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-05-30 21:00:00+00:00,FR04014,no2,26.9,µg/m³ +Paris,FR,2019-05-30 20:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-05-30 19:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-30 18:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-30 17:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-30 16:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-30 15:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-30 14:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-30 13:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-05-30 12:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-05-30 11:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-30 10:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-05-30 09:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-30 08:00:00+00:00,FR04014,no2,16.7,µg/m³ +Paris,FR,2019-05-30 07:00:00+00:00,FR04014,no2,18.3,µg/m³ +Paris,FR,2019-05-30 06:00:00+00:00,FR04014,no2,13.3,µg/m³ +Paris,FR,2019-05-30 05:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-05-30 04:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-05-30 03:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-30 02:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-05-30 01:00:00+00:00,FR04014,no2,12.4,µg/m³ +Paris,FR,2019-05-30 00:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-05-29 23:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-05-29 22:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-29 21:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-05-29 20:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-05-29 19:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-29 18:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-29 17:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-29 16:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-29 15:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-29 14:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-29 13:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-29 12:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-05-29 11:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-05-29 10:00:00+00:00,FR04014,no2,30.7,µg/m³ +Paris,FR,2019-05-29 09:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-29 08:00:00+00:00,FR04014,no2,45.7,µg/m³ +Paris,FR,2019-05-29 07:00:00+00:00,FR04014,no2,50.5,µg/m³ +Paris,FR,2019-05-29 06:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-29 05:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-05-29 04:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-29 03:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-29 02:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-29 01:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-05-29 00:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-05-28 23:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-28 22:00:00+00:00,FR04014,no2,20.2,µg/m³ +Paris,FR,2019-05-28 21:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-28 20:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-28 19:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-28 18:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-05-28 17:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-05-28 16:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-05-28 15:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-05-28 14:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-28 13:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-28 12:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-05-28 11:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-28 10:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-05-28 09:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-28 08:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-05-28 07:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-05-28 06:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-05-28 05:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-05-28 04:00:00+00:00,FR04014,no2,8.9,µg/m³ +Paris,FR,2019-05-28 03:00:00+00:00,FR04014,no2,6.1,µg/m³ +Paris,FR,2019-05-28 02:00:00+00:00,FR04014,no2,6.4,µg/m³ +Paris,FR,2019-05-28 01:00:00+00:00,FR04014,no2,8.2,µg/m³ +Paris,FR,2019-05-28 00:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-27 23:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-05-27 22:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-05-27 21:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-27 20:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-27 19:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-27 18:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-05-27 17:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-27 16:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-05-27 15:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-05-27 14:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-05-27 13:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-27 12:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-05-27 11:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-05-27 10:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-27 09:00:00+00:00,FR04014,no2,31.4,µg/m³ +Paris,FR,2019-05-27 08:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-05-27 07:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-05-27 06:00:00+00:00,FR04014,no2,29.1,µg/m³ +Paris,FR,2019-05-27 05:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-27 04:00:00+00:00,FR04014,no2,6.5,µg/m³ +Paris,FR,2019-05-27 03:00:00+00:00,FR04014,no2,4.8,µg/m³ +Paris,FR,2019-05-27 02:00:00+00:00,FR04014,no2,5.9,µg/m³ +Paris,FR,2019-05-27 01:00:00+00:00,FR04014,no2,7.1,µg/m³ +Paris,FR,2019-05-27 00:00:00+00:00,FR04014,no2,9.5,µg/m³ +Paris,FR,2019-05-26 23:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-26 22:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-05-26 21:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-05-26 20:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-05-26 19:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-26 18:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-05-26 17:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-05-26 16:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-05-26 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-26 14:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-26 13:00:00+00:00,FR04014,no2,12.5,µg/m³ +Paris,FR,2019-05-26 12:00:00+00:00,FR04014,no2,11.5,µg/m³ +Paris,FR,2019-05-26 11:00:00+00:00,FR04014,no2,13.3,µg/m³ +Paris,FR,2019-05-26 10:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-05-26 09:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-26 08:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-05-26 07:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-05-26 06:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-26 05:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-05-26 04:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-26 03:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-26 02:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-05-26 01:00:00+00:00,FR04014,no2,49.8,µg/m³ +Paris,FR,2019-05-26 00:00:00+00:00,FR04014,no2,67.0,µg/m³ +Paris,FR,2019-05-25 23:00:00+00:00,FR04014,no2,70.2,µg/m³ +Paris,FR,2019-05-25 22:00:00+00:00,FR04014,no2,63.9,µg/m³ +Paris,FR,2019-05-25 21:00:00+00:00,FR04014,no2,39.5,µg/m³ +Paris,FR,2019-05-25 20:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-05-25 19:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-25 18:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-05-25 17:00:00+00:00,FR04014,no2,20.6,µg/m³ +Paris,FR,2019-05-25 16:00:00+00:00,FR04014,no2,31.9,µg/m³ +Paris,FR,2019-05-25 15:00:00+00:00,FR04014,no2,30.0,µg/m³ +Paris,FR,2019-05-25 14:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-05-25 13:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-25 12:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-05-25 11:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-25 10:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-05-25 09:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-05-25 08:00:00+00:00,FR04014,no2,44.5,µg/m³ +Paris,FR,2019-05-25 07:00:00+00:00,FR04014,no2,42.1,µg/m³ +Paris,FR,2019-05-25 06:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-05-25 02:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-25 01:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-25 00:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-05-24 23:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-05-24 22:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-05-24 21:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-05-24 20:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-05-24 19:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-05-24 18:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-24 17:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-24 16:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-05-24 15:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-24 14:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-24 13:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-24 12:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-05-24 11:00:00+00:00,FR04014,no2,40.6,µg/m³ +Paris,FR,2019-05-24 10:00:00+00:00,FR04014,no2,28.6,µg/m³ +Paris,FR,2019-05-24 09:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-05-24 08:00:00+00:00,FR04014,no2,45.9,µg/m³ +Paris,FR,2019-05-24 07:00:00+00:00,FR04014,no2,54.8,µg/m³ +Paris,FR,2019-05-24 06:00:00+00:00,FR04014,no2,40.7,µg/m³ +Paris,FR,2019-05-24 05:00:00+00:00,FR04014,no2,35.9,µg/m³ +Paris,FR,2019-05-24 04:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-05-24 03:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-05-24 02:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-24 01:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-05-24 00:00:00+00:00,FR04014,no2,32.8,µg/m³ +Paris,FR,2019-05-23 23:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-23 22:00:00+00:00,FR04014,no2,61.9,µg/m³ +Paris,FR,2019-05-23 21:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-05-23 20:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-05-23 19:00:00+00:00,FR04014,no2,28.0,µg/m³ +Paris,FR,2019-05-23 18:00:00+00:00,FR04014,no2,23.5,µg/m³ +Paris,FR,2019-05-23 17:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-23 16:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-23 15:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-23 14:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-23 13:00:00+00:00,FR04014,no2,21.2,µg/m³ +Paris,FR,2019-05-23 12:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-05-23 11:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-05-23 10:00:00+00:00,FR04014,no2,28.3,µg/m³ +Paris,FR,2019-05-23 09:00:00+00:00,FR04014,no2,79.4,µg/m³ +Paris,FR,2019-05-23 08:00:00+00:00,FR04014,no2,97.0,µg/m³ +Paris,FR,2019-05-23 07:00:00+00:00,FR04014,no2,91.8,µg/m³ +Paris,FR,2019-05-23 06:00:00+00:00,FR04014,no2,79.6,µg/m³ +Paris,FR,2019-05-23 05:00:00+00:00,FR04014,no2,68.7,µg/m³ +Paris,FR,2019-05-23 04:00:00+00:00,FR04014,no2,71.9,µg/m³ +Paris,FR,2019-05-23 03:00:00+00:00,FR04014,no2,76.8,µg/m³ +Paris,FR,2019-05-23 02:00:00+00:00,FR04014,no2,66.6,µg/m³ +Paris,FR,2019-05-23 01:00:00+00:00,FR04014,no2,53.1,µg/m³ +Paris,FR,2019-05-23 00:00:00+00:00,FR04014,no2,53.3,µg/m³ +Paris,FR,2019-05-22 23:00:00+00:00,FR04014,no2,62.1,µg/m³ +Paris,FR,2019-05-22 22:00:00+00:00,FR04014,no2,29.8,µg/m³ +Paris,FR,2019-05-22 21:00:00+00:00,FR04014,no2,37.7,µg/m³ +Paris,FR,2019-05-22 20:00:00+00:00,FR04014,no2,44.9,µg/m³ +Paris,FR,2019-05-22 19:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-05-22 18:00:00+00:00,FR04014,no2,34.1,µg/m³ +Paris,FR,2019-05-22 17:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-05-22 16:00:00+00:00,FR04014,no2,34.9,µg/m³ +Paris,FR,2019-05-22 15:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-05-22 14:00:00+00:00,FR04014,no2,40.0,µg/m³ +Paris,FR,2019-05-22 13:00:00+00:00,FR04014,no2,38.5,µg/m³ +Paris,FR,2019-05-22 12:00:00+00:00,FR04014,no2,42.2,µg/m³ +Paris,FR,2019-05-22 11:00:00+00:00,FR04014,no2,42.6,µg/m³ +Paris,FR,2019-05-22 10:00:00+00:00,FR04014,no2,57.8,µg/m³ +Paris,FR,2019-05-22 09:00:00+00:00,FR04014,no2,63.1,µg/m³ +Paris,FR,2019-05-22 08:00:00+00:00,FR04014,no2,70.8,µg/m³ +Paris,FR,2019-05-22 07:00:00+00:00,FR04014,no2,75.4,µg/m³ +Paris,FR,2019-05-22 06:00:00+00:00,FR04014,no2,75.7,µg/m³ +Paris,FR,2019-05-22 05:00:00+00:00,FR04014,no2,45.1,µg/m³ +Paris,FR,2019-05-22 04:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-05-22 03:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-22 02:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-05-22 01:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-22 00:00:00+00:00,FR04014,no2,27.1,µg/m³ +Paris,FR,2019-05-21 23:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-05-21 22:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-05-21 21:00:00+00:00,FR04014,no2,43.0,µg/m³ +Paris,FR,2019-05-21 20:00:00+00:00,FR04014,no2,40.8,µg/m³ +Paris,FR,2019-05-21 19:00:00+00:00,FR04014,no2,50.0,µg/m³ +Paris,FR,2019-05-21 18:00:00+00:00,FR04014,no2,54.3,µg/m³ +Paris,FR,2019-05-21 17:00:00+00:00,FR04014,no2,75.0,µg/m³ +Paris,FR,2019-05-21 16:00:00+00:00,FR04014,no2,42.3,µg/m³ +Paris,FR,2019-05-21 15:00:00+00:00,FR04014,no2,36.6,µg/m³ +Paris,FR,2019-05-21 14:00:00+00:00,FR04014,no2,47.8,µg/m³ +Paris,FR,2019-05-21 13:00:00+00:00,FR04014,no2,49.7,µg/m³ +Paris,FR,2019-05-21 12:00:00+00:00,FR04014,no2,30.5,µg/m³ +Paris,FR,2019-05-21 11:00:00+00:00,FR04014,no2,25.5,µg/m³ +Paris,FR,2019-05-21 10:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-05-21 09:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-05-21 08:00:00+00:00,FR04014,no2,54.2,µg/m³ +Paris,FR,2019-05-21 07:00:00+00:00,FR04014,no2,56.0,µg/m³ +Paris,FR,2019-05-21 06:00:00+00:00,FR04014,no2,62.6,µg/m³ +Paris,FR,2019-05-21 05:00:00+00:00,FR04014,no2,38.0,µg/m³ +Paris,FR,2019-05-21 04:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-21 03:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-05-21 02:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-05-21 01:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-05-21 00:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-05-20 23:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-05-20 22:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-05-20 21:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-20 20:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-05-20 19:00:00+00:00,FR04014,no2,21.3,µg/m³ +Paris,FR,2019-05-20 18:00:00+00:00,FR04014,no2,32.2,µg/m³ +Paris,FR,2019-05-20 17:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-20 16:00:00+00:00,FR04014,no2,32.4,µg/m³ +Paris,FR,2019-05-20 15:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-05-20 14:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-05-20 13:00:00+00:00,FR04014,no2,23.7,µg/m³ +Paris,FR,2019-05-20 12:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-20 11:00:00+00:00,FR04014,no2,35.4,µg/m³ +Paris,FR,2019-05-20 10:00:00+00:00,FR04014,no2,43.9,µg/m³ +Paris,FR,2019-05-20 09:00:00+00:00,FR04014,no2,45.5,µg/m³ +Paris,FR,2019-05-20 08:00:00+00:00,FR04014,no2,46.1,µg/m³ +Paris,FR,2019-05-20 07:00:00+00:00,FR04014,no2,46.9,µg/m³ +Paris,FR,2019-05-20 06:00:00+00:00,FR04014,no2,40.1,µg/m³ +Paris,FR,2019-05-20 05:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-20 04:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-20 03:00:00+00:00,FR04014,no2,12.6,µg/m³ +Paris,FR,2019-05-20 02:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-05-20 01:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-20 00:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-05-19 23:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-19 22:00:00+00:00,FR04014,no2,22.2,µg/m³ +Paris,FR,2019-05-19 21:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-19 20:00:00+00:00,FR04014,no2,35.6,µg/m³ +Paris,FR,2019-05-19 19:00:00+00:00,FR04014,no2,51.2,µg/m³ +Paris,FR,2019-05-19 18:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-05-19 17:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-05-19 16:00:00+00:00,FR04014,no2,32.5,µg/m³ +Paris,FR,2019-05-19 15:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-05-19 14:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-19 13:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-05-19 12:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-19 11:00:00+00:00,FR04014,no2,32.6,µg/m³ +Paris,FR,2019-05-19 10:00:00+00:00,FR04014,no2,31.0,µg/m³ +Paris,FR,2019-05-19 09:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-05-19 08:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-05-19 07:00:00+00:00,FR04014,no2,32.4,µg/m³ +Paris,FR,2019-05-19 06:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-05-19 05:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-05-19 04:00:00+00:00,FR04014,no2,39.4,µg/m³ +Paris,FR,2019-05-19 03:00:00+00:00,FR04014,no2,36.4,µg/m³ +Paris,FR,2019-05-19 02:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-05-19 01:00:00+00:00,FR04014,no2,34.9,µg/m³ +Paris,FR,2019-05-19 00:00:00+00:00,FR04014,no2,49.6,µg/m³ +Paris,FR,2019-05-18 23:00:00+00:00,FR04014,no2,50.2,µg/m³ +Paris,FR,2019-05-18 22:00:00+00:00,FR04014,no2,62.5,µg/m³ +Paris,FR,2019-05-18 21:00:00+00:00,FR04014,no2,59.3,µg/m³ +Paris,FR,2019-05-18 20:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-05-18 19:00:00+00:00,FR04014,no2,67.5,µg/m³ +Paris,FR,2019-05-18 18:00:00+00:00,FR04014,no2,14.5,µg/m³ +Paris,FR,2019-05-18 17:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-18 16:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-05-18 15:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-18 14:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-05-18 13:00:00+00:00,FR04014,no2,10.5,µg/m³ +Paris,FR,2019-05-18 12:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-18 11:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-18 10:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-18 09:00:00+00:00,FR04014,no2,21.1,µg/m³ +Paris,FR,2019-05-18 08:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-18 07:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-05-18 06:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-18 05:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-18 04:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-05-18 03:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-05-18 02:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-18 01:00:00+00:00,FR04014,no2,37.4,µg/m³ +Paris,FR,2019-05-18 00:00:00+00:00,FR04014,no2,31.5,µg/m³ +Paris,FR,2019-05-17 23:00:00+00:00,FR04014,no2,34.1,µg/m³ +Paris,FR,2019-05-17 22:00:00+00:00,FR04014,no2,28.2,µg/m³ +Paris,FR,2019-05-17 21:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-17 20:00:00+00:00,FR04014,no2,23.5,µg/m³ +Paris,FR,2019-05-17 19:00:00+00:00,FR04014,no2,24.7,µg/m³ +Paris,FR,2019-05-17 18:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-05-17 17:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-17 16:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-05-17 15:00:00+00:00,FR04014,no2,22.2,µg/m³ +Paris,FR,2019-05-17 14:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-05-17 13:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-05-17 12:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-17 11:00:00+00:00,FR04014,no2,43.1,µg/m³ +Paris,FR,2019-05-17 10:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-05-17 09:00:00+00:00,FR04014,no2,60.5,µg/m³ +Paris,FR,2019-05-17 08:00:00+00:00,FR04014,no2,57.5,µg/m³ +Paris,FR,2019-05-17 07:00:00+00:00,FR04014,no2,55.0,µg/m³ +Paris,FR,2019-05-17 06:00:00+00:00,FR04014,no2,46.3,µg/m³ +Paris,FR,2019-05-17 05:00:00+00:00,FR04014,no2,34.0,µg/m³ +Paris,FR,2019-05-17 04:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-17 03:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-05-17 02:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-17 01:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-17 00:00:00+00:00,FR04014,no2,46.3,µg/m³ +Paris,FR,2019-05-16 23:00:00+00:00,FR04014,no2,43.7,µg/m³ +Paris,FR,2019-05-16 22:00:00+00:00,FR04014,no2,37.1,µg/m³ +Paris,FR,2019-05-16 21:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-16 20:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-05-16 19:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-05-16 18:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-05-16 17:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-05-16 16:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-16 15:00:00+00:00,FR04014,no2,10.1,µg/m³ +Paris,FR,2019-05-16 14:00:00+00:00,FR04014,no2,8.1,µg/m³ +Paris,FR,2019-05-16 13:00:00+00:00,FR04014,no2,8.5,µg/m³ +Paris,FR,2019-05-16 12:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-05-16 11:00:00+00:00,FR04014,no2,10.5,µg/m³ +Paris,FR,2019-05-16 10:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-05-16 09:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-05-16 08:00:00+00:00,FR04014,no2,39.4,µg/m³ +Paris,FR,2019-05-16 07:00:00+00:00,FR04014,no2,40.0,µg/m³ +Paris,FR,2019-05-16 05:00:00+00:00,FR04014,no2,52.6,µg/m³ +Paris,FR,2019-05-16 04:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-16 03:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-16 02:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-05-16 01:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-05-16 00:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-05-15 23:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-05-15 22:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-05-15 21:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-05-15 20:00:00+00:00,FR04014,no2,30.1,µg/m³ +Paris,FR,2019-05-15 19:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-15 18:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-05-15 17:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-15 16:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-05-15 15:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-15 14:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-05-15 13:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-05-15 12:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-05-15 11:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-15 10:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-15 09:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-15 08:00:00+00:00,FR04014,no2,25.7,µg/m³ +Paris,FR,2019-05-15 07:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-05-15 06:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-05-15 05:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-15 04:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-05-15 03:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-05-15 02:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-05-15 01:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-15 00:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-14 23:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-14 22:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-05-14 21:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-14 20:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-14 19:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-14 18:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-05-14 17:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-05-14 16:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-14 15:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-05-14 14:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-05-14 13:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-05-14 12:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-05-14 11:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-05-14 10:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-14 09:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-14 08:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-05-14 07:00:00+00:00,FR04014,no2,41.3,µg/m³ +Paris,FR,2019-05-14 06:00:00+00:00,FR04014,no2,46.1,µg/m³ +Paris,FR,2019-05-14 05:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-14 04:00:00+00:00,FR04014,no2,31.6,µg/m³ +Paris,FR,2019-05-14 03:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-14 02:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-14 01:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-05-14 00:00:00+00:00,FR04014,no2,20.9,µg/m³ +Paris,FR,2019-05-13 23:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-05-13 22:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-05-13 21:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-05-13 20:00:00+00:00,FR04014,no2,28.3,µg/m³ +Paris,FR,2019-05-13 19:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-05-13 18:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-05-13 17:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-13 16:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-05-13 15:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-13 14:00:00+00:00,FR04014,no2,10.7,µg/m³ +Paris,FR,2019-05-13 13:00:00+00:00,FR04014,no2,10.1,µg/m³ +Paris,FR,2019-05-13 12:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-05-13 11:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-05-13 10:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-13 09:00:00+00:00,FR04014,no2,20.6,µg/m³ +Paris,FR,2019-05-13 08:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-05-13 07:00:00+00:00,FR04014,no2,41.0,µg/m³ +Paris,FR,2019-05-13 06:00:00+00:00,FR04014,no2,45.2,µg/m³ +Paris,FR,2019-05-13 05:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-05-13 04:00:00+00:00,FR04014,no2,25.1,µg/m³ +Paris,FR,2019-05-13 03:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-05-13 02:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-13 01:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-05-13 00:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-05-12 23:00:00+00:00,FR04014,no2,32.5,µg/m³ +Paris,FR,2019-05-12 22:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-12 21:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-05-12 20:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-12 19:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-12 18:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-05-12 17:00:00+00:00,FR04014,no2,13.9,µg/m³ +Paris,FR,2019-05-12 16:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-12 15:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-05-12 14:00:00+00:00,FR04014,no2,9.1,µg/m³ +Paris,FR,2019-05-12 13:00:00+00:00,FR04014,no2,8.7,µg/m³ +Paris,FR,2019-05-12 12:00:00+00:00,FR04014,no2,10.9,µg/m³ +Paris,FR,2019-05-12 11:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-05-12 10:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-05-12 09:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-12 08:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-05-12 07:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-05-12 06:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-12 05:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-05-12 04:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-05-12 03:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-05-12 02:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-12 01:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-05-12 00:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-05-11 23:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-05-11 22:00:00+00:00,FR04014,no2,27.7,µg/m³ +Paris,FR,2019-05-11 21:00:00+00:00,FR04014,no2,21.1,µg/m³ +Paris,FR,2019-05-11 20:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-05-11 19:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-05-11 18:00:00+00:00,FR04014,no2,33.1,µg/m³ +Paris,FR,2019-05-11 17:00:00+00:00,FR04014,no2,32.0,µg/m³ +Paris,FR,2019-05-11 16:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-11 15:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-05-11 14:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-11 13:00:00+00:00,FR04014,no2,30.8,µg/m³ +Paris,FR,2019-05-11 12:00:00+00:00,FR04014,no2,30.2,µg/m³ +Paris,FR,2019-05-11 11:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-05-11 10:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-05-11 09:00:00+00:00,FR04014,no2,35.7,µg/m³ +Paris,FR,2019-05-11 08:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-05-11 07:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-11 06:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-05-11 02:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-11 01:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-05-11 00:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-05-10 23:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-05-10 22:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-05-10 21:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-10 20:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-05-10 19:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-05-10 18:00:00+00:00,FR04014,no2,33.4,µg/m³ +Paris,FR,2019-05-10 17:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-05-10 16:00:00+00:00,FR04014,no2,30.8,µg/m³ +Paris,FR,2019-05-10 15:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-05-10 14:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-05-10 13:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-05-10 12:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-10 11:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-10 10:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-05-10 09:00:00+00:00,FR04014,no2,53.4,µg/m³ +Paris,FR,2019-05-10 08:00:00+00:00,FR04014,no2,60.7,µg/m³ +Paris,FR,2019-05-10 07:00:00+00:00,FR04014,no2,57.3,µg/m³ +Paris,FR,2019-05-10 06:00:00+00:00,FR04014,no2,47.4,µg/m³ +Paris,FR,2019-05-10 05:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-05-10 04:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-10 03:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-05-10 02:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-05-10 01:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-05-10 00:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-09 23:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-05-09 22:00:00+00:00,FR04014,no2,29.7,µg/m³ +Paris,FR,2019-05-09 21:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-09 20:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-05-09 19:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-09 18:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-05-09 17:00:00+00:00,FR04014,no2,29.9,µg/m³ +Paris,FR,2019-05-09 16:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-05-09 15:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-05-09 14:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-09 13:00:00+00:00,FR04014,no2,21.3,µg/m³ +Paris,FR,2019-05-09 12:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-05-09 11:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-05-09 10:00:00+00:00,FR04014,no2,43.1,µg/m³ +Paris,FR,2019-05-09 09:00:00+00:00,FR04014,no2,32.3,µg/m³ +Paris,FR,2019-05-09 08:00:00+00:00,FR04014,no2,32.2,µg/m³ +Paris,FR,2019-05-09 07:00:00+00:00,FR04014,no2,49.0,µg/m³ +Paris,FR,2019-05-09 06:00:00+00:00,FR04014,no2,50.7,µg/m³ +Paris,FR,2019-05-09 05:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-09 04:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-09 03:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-05-09 02:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-05-09 01:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-09 00:00:00+00:00,FR04014,no2,14.7,µg/m³ +Paris,FR,2019-05-08 23:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-08 22:00:00+00:00,FR04014,no2,32.2,µg/m³ +Paris,FR,2019-05-08 21:00:00+00:00,FR04014,no2,48.9,µg/m³ +Paris,FR,2019-05-08 20:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-05-08 19:00:00+00:00,FR04014,no2,41.3,µg/m³ +Paris,FR,2019-05-08 18:00:00+00:00,FR04014,no2,27.8,µg/m³ +Paris,FR,2019-05-08 17:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-05-08 16:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-08 15:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-05-08 14:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-08 13:00:00+00:00,FR04014,no2,14.3,µg/m³ +Paris,FR,2019-05-08 12:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-08 11:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-05-08 10:00:00+00:00,FR04014,no2,33.4,µg/m³ +Paris,FR,2019-05-08 09:00:00+00:00,FR04014,no2,19.7,µg/m³ +Paris,FR,2019-05-08 08:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-05-08 07:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-08 06:00:00+00:00,FR04014,no2,21.7,µg/m³ +Paris,FR,2019-05-08 05:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-05-08 04:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-05-08 03:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-05-08 02:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-08 01:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-05-08 00:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-07 23:00:00+00:00,FR04014,no2,34.0,µg/m³ +Paris,FR,2019-05-07 22:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-05-07 21:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-05-07 20:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-05-07 19:00:00+00:00,FR04014,no2,26.8,µg/m³ +Paris,FR,2019-05-07 18:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-05-07 17:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-07 16:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-05-07 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-07 14:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-05-07 13:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-05-07 12:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-07 11:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-07 10:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-07 09:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-07 08:00:00+00:00,FR04014,no2,56.0,µg/m³ +Paris,FR,2019-05-07 07:00:00+00:00,FR04014,no2,67.9,µg/m³ +Paris,FR,2019-05-07 06:00:00+00:00,FR04014,no2,77.7,µg/m³ +Paris,FR,2019-05-07 05:00:00+00:00,FR04014,no2,72.4,µg/m³ +Paris,FR,2019-05-07 04:00:00+00:00,FR04014,no2,61.9,µg/m³ +Paris,FR,2019-05-07 03:00:00+00:00,FR04014,no2,50.4,µg/m³ +Paris,FR,2019-05-07 02:00:00+00:00,FR04014,no2,27.7,µg/m³ +Paris,FR,2019-05-07 01:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-05-07 00:00:00+00:00,FR04014,no2,47.2,µg/m³ +Paris,FR,2019-05-06 23:00:00+00:00,FR04014,no2,53.1,µg/m³ +Paris,FR,2019-05-06 22:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-06 21:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-05-06 20:00:00+00:00,FR04014,no2,35.9,µg/m³ +Paris,FR,2019-05-06 19:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-05-06 18:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-06 17:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-05-06 16:00:00+00:00,FR04014,no2,38.4,µg/m³ +Paris,FR,2019-05-06 15:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-05-06 14:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-05-06 13:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-06 12:00:00+00:00,FR04014,no2,42.1,µg/m³ +Paris,FR,2019-05-06 11:00:00+00:00,FR04014,no2,44.3,µg/m³ +Paris,FR,2019-05-06 10:00:00+00:00,FR04014,no2,42.4,µg/m³ +Paris,FR,2019-05-06 09:00:00+00:00,FR04014,no2,44.2,µg/m³ +Paris,FR,2019-05-06 08:00:00+00:00,FR04014,no2,52.5,µg/m³ +Paris,FR,2019-05-06 07:00:00+00:00,FR04014,no2,68.9,µg/m³ +Paris,FR,2019-05-06 06:00:00+00:00,FR04014,no2,62.4,µg/m³ +Paris,FR,2019-05-06 05:00:00+00:00,FR04014,no2,56.7,µg/m³ +Paris,FR,2019-05-06 04:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-05-06 03:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-05-06 02:00:00+00:00,FR04014,no2,25.1,µg/m³ +Paris,FR,2019-05-06 01:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-05-06 00:00:00+00:00,FR04014,no2,26.8,µg/m³ +Paris,FR,2019-05-05 23:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-05-05 22:00:00+00:00,FR04014,no2,28.6,µg/m³ +Paris,FR,2019-05-05 21:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-05-05 20:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-05-05 19:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-05 18:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-05 17:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-05 16:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-05-05 15:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-05-05 14:00:00+00:00,FR04014,no2,17.6,µg/m³ +Paris,FR,2019-05-05 13:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-05 12:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-05 11:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-05-05 10:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-05 09:00:00+00:00,FR04014,no2,11.6,µg/m³ +Paris,FR,2019-05-05 08:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-05-05 07:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-05-05 06:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-05-05 05:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-05-05 04:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-05 03:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-05 02:00:00+00:00,FR04014,no2,27.2,µg/m³ +Paris,FR,2019-05-05 01:00:00+00:00,FR04014,no2,25.7,µg/m³ +Paris,FR,2019-05-05 00:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-04 23:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-05-04 22:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-05-04 21:00:00+00:00,FR04014,no2,27.1,µg/m³ +Paris,FR,2019-05-04 20:00:00+00:00,FR04014,no2,33.1,µg/m³ +Paris,FR,2019-05-04 19:00:00+00:00,FR04014,no2,26.8,µg/m³ +Paris,FR,2019-05-04 18:00:00+00:00,FR04014,no2,16.7,µg/m³ +Paris,FR,2019-05-04 17:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-05-04 16:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-05-04 15:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-05-04 14:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-05-04 13:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-05-04 12:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-05-04 11:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-05-04 10:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-05-04 09:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-04 08:00:00+00:00,FR04014,no2,22.5,µg/m³ +Paris,FR,2019-05-04 07:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-05-04 06:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-04 05:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-05-04 04:00:00+00:00,FR04014,no2,20.0,µg/m³ +Paris,FR,2019-05-04 03:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-04 02:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-04 01:00:00+00:00,FR04014,no2,22.2,µg/m³ +Paris,FR,2019-05-04 00:00:00+00:00,FR04014,no2,29.7,µg/m³ +Paris,FR,2019-05-03 23:00:00+00:00,FR04014,no2,31.3,µg/m³ +Paris,FR,2019-05-03 22:00:00+00:00,FR04014,no2,43.2,µg/m³ +Paris,FR,2019-05-03 21:00:00+00:00,FR04014,no2,31.8,µg/m³ +Paris,FR,2019-05-03 20:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-03 19:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-05-03 18:00:00+00:00,FR04014,no2,59.6,µg/m³ +Paris,FR,2019-05-03 17:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-03 16:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-05-03 15:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-05-03 14:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-05-03 13:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-05-03 12:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-03 11:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-05-03 10:00:00+00:00,FR04014,no2,46.3,µg/m³ +Paris,FR,2019-05-03 09:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-05-03 08:00:00+00:00,FR04014,no2,46.4,µg/m³ +Paris,FR,2019-05-03 07:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-05-03 06:00:00+00:00,FR04014,no2,45.1,µg/m³ +Paris,FR,2019-05-03 05:00:00+00:00,FR04014,no2,32.8,µg/m³ +Paris,FR,2019-05-03 04:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-03 03:00:00+00:00,FR04014,no2,17.6,µg/m³ +Paris,FR,2019-05-03 02:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-03 01:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-03 00:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-05-02 23:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-05-02 22:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-05-02 21:00:00+00:00,FR04014,no2,31.0,µg/m³ +Paris,FR,2019-05-02 20:00:00+00:00,FR04014,no2,28.6,µg/m³ +Paris,FR,2019-05-02 19:00:00+00:00,FR04014,no2,30.7,µg/m³ +Paris,FR,2019-05-02 18:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-02 17:00:00+00:00,FR04014,no2,29.9,µg/m³ +Paris,FR,2019-05-02 16:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-05-02 15:00:00+00:00,FR04014,no2,41.4,µg/m³ +Paris,FR,2019-05-02 14:00:00+00:00,FR04014,no2,36.3,µg/m³ +Paris,FR,2019-05-02 13:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-05-02 12:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-02 11:00:00+00:00,FR04014,no2,32.6,µg/m³ +Paris,FR,2019-05-02 10:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-05-02 09:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-05-02 08:00:00+00:00,FR04014,no2,55.5,µg/m³ +Paris,FR,2019-05-02 07:00:00+00:00,FR04014,no2,51.0,µg/m³ +Paris,FR,2019-05-02 06:00:00+00:00,FR04014,no2,49.4,µg/m³ +Paris,FR,2019-05-02 05:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-05-02 04:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-02 03:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-02 02:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-05-02 01:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-05-02 00:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-05-01 23:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-01 22:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-01 21:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-05-01 20:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-01 19:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-01 18:00:00+00:00,FR04014,no2,23.0,µg/m³ +Paris,FR,2019-05-01 17:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-01 16:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-05-01 15:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-05-01 14:00:00+00:00,FR04014,no2,20.6,µg/m³ +Paris,FR,2019-05-01 13:00:00+00:00,FR04014,no2,22.5,µg/m³ +Paris,FR,2019-05-01 12:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-01 11:00:00+00:00,FR04014,no2,28.2,µg/m³ +Paris,FR,2019-05-01 10:00:00+00:00,FR04014,no2,33.3,µg/m³ +Paris,FR,2019-05-01 09:00:00+00:00,FR04014,no2,33.5,µg/m³ +Paris,FR,2019-05-01 08:00:00+00:00,FR04014,no2,33.5,µg/m³ +Paris,FR,2019-05-01 07:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-05-01 06:00:00+00:00,FR04014,no2,33.4,µg/m³ +Paris,FR,2019-05-01 05:00:00+00:00,FR04014,no2,28.5,µg/m³ +Paris,FR,2019-05-01 04:00:00+00:00,FR04014,no2,24.9,µg/m³ +Paris,FR,2019-05-01 03:00:00+00:00,FR04014,no2,23.1,µg/m³ +Paris,FR,2019-05-01 02:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-01 01:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-05-01 00:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-04-30 23:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-04-30 22:00:00+00:00,FR04014,no2,41.3,µg/m³ +Paris,FR,2019-04-30 21:00:00+00:00,FR04014,no2,42.8,µg/m³ +Paris,FR,2019-04-30 20:00:00+00:00,FR04014,no2,39.6,µg/m³ +Paris,FR,2019-04-30 19:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-04-30 18:00:00+00:00,FR04014,no2,27.2,µg/m³ +Paris,FR,2019-04-30 17:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-04-30 16:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-04-30 15:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-04-30 14:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-04-30 13:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-04-30 12:00:00+00:00,FR04014,no2,21.5,µg/m³ +Paris,FR,2019-04-30 11:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-04-30 10:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-04-30 09:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-04-30 08:00:00+00:00,FR04014,no2,45.1,µg/m³ +Paris,FR,2019-04-30 07:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-04-30 06:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-04-30 05:00:00+00:00,FR04014,no2,37.3,µg/m³ +Paris,FR,2019-04-30 04:00:00+00:00,FR04014,no2,30.8,µg/m³ +Paris,FR,2019-04-30 03:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-04-30 02:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-04-30 01:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-04-30 00:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-04-29 23:00:00+00:00,FR04014,no2,34.3,µg/m³ +Paris,FR,2019-04-29 22:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-04-29 21:00:00+00:00,FR04014,no2,31.6,µg/m³ +Paris,FR,2019-04-29 20:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-04-29 19:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-04-29 18:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-04-29 17:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-04-29 16:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-04-29 15:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-04-29 14:00:00+00:00,FR04014,no2,15.7,µg/m³ +Paris,FR,2019-04-29 13:00:00+00:00,FR04014,no2,14.3,µg/m³ +Paris,FR,2019-04-29 12:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-04-29 11:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-04-29 10:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-04-29 09:00:00+00:00,FR04014,no2,28.5,µg/m³ +Paris,FR,2019-04-29 08:00:00+00:00,FR04014,no2,39.1,µg/m³ +Paris,FR,2019-04-29 07:00:00+00:00,FR04014,no2,45.4,µg/m³ +Paris,FR,2019-04-29 06:00:00+00:00,FR04014,no2,52.6,µg/m³ +Paris,FR,2019-04-29 05:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-04-29 04:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-04-29 03:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-04-29 02:00:00+00:00,FR04014,no2,34.9,µg/m³ +Paris,FR,2019-04-29 01:00:00+00:00,FR04014,no2,25.5,µg/m³ +Paris,FR,2019-04-29 00:00:00+00:00,FR04014,no2,26.2,µg/m³ +Paris,FR,2019-04-28 23:00:00+00:00,FR04014,no2,29.8,µg/m³ +Paris,FR,2019-04-28 22:00:00+00:00,FR04014,no2,27.1,µg/m³ +Paris,FR,2019-04-28 21:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-04-28 20:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-04-28 19:00:00+00:00,FR04014,no2,32.3,µg/m³ +Paris,FR,2019-04-28 18:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-04-28 17:00:00+00:00,FR04014,no2,23.7,µg/m³ +Paris,FR,2019-04-28 16:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-04-28 15:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-04-28 14:00:00+00:00,FR04014,no2,18.4,µg/m³ +Paris,FR,2019-04-28 13:00:00+00:00,FR04014,no2,19.8,µg/m³ +Paris,FR,2019-04-28 12:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-04-28 11:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-04-28 10:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-04-28 09:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-04-28 08:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-04-28 07:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-04-28 06:00:00+00:00,FR04014,no2,13.6,µg/m³ +Paris,FR,2019-04-28 05:00:00+00:00,FR04014,no2,12.7,µg/m³ +Paris,FR,2019-04-28 04:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-04-28 03:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-04-28 02:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-04-28 01:00:00+00:00,FR04014,no2,12.3,µg/m³ +Paris,FR,2019-04-28 00:00:00+00:00,FR04014,no2,14.8,µg/m³ +Paris,FR,2019-04-27 23:00:00+00:00,FR04014,no2,18.7,µg/m³ +Paris,FR,2019-04-27 22:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-04-27 21:00:00+00:00,FR04014,no2,16.7,µg/m³ +Paris,FR,2019-04-27 20:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-04-27 19:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-04-27 18:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-04-27 17:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-04-27 16:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-04-27 15:00:00+00:00,FR04014,no2,13.7,µg/m³ +Paris,FR,2019-04-27 14:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-04-27 13:00:00+00:00,FR04014,no2,13.9,µg/m³ +Paris,FR,2019-04-27 12:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-04-27 11:00:00+00:00,FR04014,no2,12.3,µg/m³ +Paris,FR,2019-04-27 10:00:00+00:00,FR04014,no2,10.9,µg/m³ +Paris,FR,2019-04-27 09:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-04-27 08:00:00+00:00,FR04014,no2,14.5,µg/m³ +Paris,FR,2019-04-27 07:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-04-27 06:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-04-27 05:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-04-27 04:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-04-27 03:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-04-27 02:00:00+00:00,FR04014,no2,8.6,µg/m³ +Paris,FR,2019-04-27 01:00:00+00:00,FR04014,no2,9.3,µg/m³ +Paris,FR,2019-04-27 00:00:00+00:00,FR04014,no2,10.8,µg/m³ +Paris,FR,2019-04-26 23:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-04-26 22:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-04-26 21:00:00+00:00,FR04014,no2,34.8,µg/m³ +Paris,FR,2019-04-26 20:00:00+00:00,FR04014,no2,38.7,µg/m³ +Paris,FR,2019-04-26 19:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-04-26 18:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-04-26 17:00:00+00:00,FR04014,no2,20.2,µg/m³ +Paris,FR,2019-04-26 16:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-04-26 15:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-04-26 14:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-04-26 13:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-04-26 12:00:00+00:00,FR04014,no2,27.2,µg/m³ +Paris,FR,2019-04-26 11:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-04-26 10:00:00+00:00,FR04014,no2,22.2,µg/m³ +Paris,FR,2019-04-26 09:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-04-26 08:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-04-26 07:00:00+00:00,FR04014,no2,47.2,µg/m³ +Paris,FR,2019-04-26 06:00:00+00:00,FR04014,no2,61.8,µg/m³ +Paris,FR,2019-04-26 05:00:00+00:00,FR04014,no2,70.9,µg/m³ +Paris,FR,2019-04-26 04:00:00+00:00,FR04014,no2,58.3,µg/m³ +Paris,FR,2019-04-26 03:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-04-26 02:00:00+00:00,FR04014,no2,27.8,µg/m³ +Paris,FR,2019-04-26 01:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-04-26 00:00:00+00:00,FR04014,no2,25.1,µg/m³ +Paris,FR,2019-04-25 23:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-04-25 22:00:00+00:00,FR04014,no2,31.0,µg/m³ +Paris,FR,2019-04-25 21:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-04-25 20:00:00+00:00,FR04014,no2,26.8,µg/m³ +Paris,FR,2019-04-25 19:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-04-25 18:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-04-25 17:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-04-25 16:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-04-25 15:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-04-25 14:00:00+00:00,FR04014,no2,21.2,µg/m³ +Paris,FR,2019-04-25 13:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-04-25 12:00:00+00:00,FR04014,no2,29.1,µg/m³ +Paris,FR,2019-04-25 11:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-04-25 10:00:00+00:00,FR04014,no2,45.1,µg/m³ +Paris,FR,2019-04-25 09:00:00+00:00,FR04014,no2,41.6,µg/m³ +Paris,FR,2019-04-25 08:00:00+00:00,FR04014,no2,37.6,µg/m³ +Paris,FR,2019-04-25 07:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-04-25 06:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-04-25 05:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-04-25 04:00:00+00:00,FR04014,no2,16.7,µg/m³ +Paris,FR,2019-04-25 03:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-04-25 02:00:00+00:00,FR04014,no2,14.8,µg/m³ +Paris,FR,2019-04-25 01:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-04-25 00:00:00+00:00,FR04014,no2,23.1,µg/m³ +Paris,FR,2019-04-24 23:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-04-24 22:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-04-24 21:00:00+00:00,FR04014,no2,40.3,µg/m³ +Paris,FR,2019-04-24 20:00:00+00:00,FR04014,no2,41.0,µg/m³ +Paris,FR,2019-04-24 19:00:00+00:00,FR04014,no2,30.7,µg/m³ +Paris,FR,2019-04-24 18:00:00+00:00,FR04014,no2,22.5,µg/m³ +Paris,FR,2019-04-24 17:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-04-24 16:00:00+00:00,FR04014,no2,31.3,µg/m³ +Paris,FR,2019-04-24 15:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-04-24 14:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-04-24 13:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-04-24 12:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-04-24 11:00:00+00:00,FR04014,no2,22.4,µg/m³ +Paris,FR,2019-04-24 10:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-04-24 09:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-04-24 08:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-04-24 07:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-04-24 06:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-04-24 05:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-04-24 04:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-04-24 03:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-04-24 02:00:00+00:00,FR04014,no2,21.2,µg/m³ +Paris,FR,2019-04-24 01:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-04-24 00:00:00+00:00,FR04014,no2,43.8,µg/m³ +Paris,FR,2019-04-23 23:00:00+00:00,FR04014,no2,48.8,µg/m³ +Paris,FR,2019-04-23 22:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-04-23 21:00:00+00:00,FR04014,no2,41.2,µg/m³ +Paris,FR,2019-04-23 20:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-04-23 19:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-04-23 18:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-04-23 17:00:00+00:00,FR04014,no2,35.7,µg/m³ +Paris,FR,2019-04-23 16:00:00+00:00,FR04014,no2,52.9,µg/m³ +Paris,FR,2019-04-23 15:00:00+00:00,FR04014,no2,44.5,µg/m³ +Paris,FR,2019-04-23 14:00:00+00:00,FR04014,no2,48.8,µg/m³ +Paris,FR,2019-04-23 13:00:00+00:00,FR04014,no2,53.2,µg/m³ +Paris,FR,2019-04-23 12:00:00+00:00,FR04014,no2,54.1,µg/m³ +Paris,FR,2019-04-23 11:00:00+00:00,FR04014,no2,51.8,µg/m³ +Paris,FR,2019-04-23 10:00:00+00:00,FR04014,no2,47.9,µg/m³ +Paris,FR,2019-04-23 09:00:00+00:00,FR04014,no2,51.9,µg/m³ +Paris,FR,2019-04-23 08:00:00+00:00,FR04014,no2,60.7,µg/m³ +Paris,FR,2019-04-23 07:00:00+00:00,FR04014,no2,86.0,µg/m³ +Paris,FR,2019-04-23 06:00:00+00:00,FR04014,no2,74.7,µg/m³ +Paris,FR,2019-04-23 05:00:00+00:00,FR04014,no2,49.2,µg/m³ +Paris,FR,2019-04-23 04:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-04-23 03:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-04-23 02:00:00+00:00,FR04014,no2,32.4,µg/m³ +Paris,FR,2019-04-23 01:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-04-23 00:00:00+00:00,FR04014,no2,35.7,µg/m³ +Paris,FR,2019-04-22 23:00:00+00:00,FR04014,no2,45.6,µg/m³ +Paris,FR,2019-04-22 22:00:00+00:00,FR04014,no2,44.5,µg/m³ +Paris,FR,2019-04-22 21:00:00+00:00,FR04014,no2,38.4,µg/m³ +Paris,FR,2019-04-22 20:00:00+00:00,FR04014,no2,31.4,µg/m³ +Paris,FR,2019-04-22 19:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-04-22 18:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-04-22 17:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-04-22 16:00:00+00:00,FR04014,no2,13.9,µg/m³ +Paris,FR,2019-04-22 15:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-04-22 14:00:00+00:00,FR04014,no2,8.9,µg/m³ +Paris,FR,2019-04-22 13:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-04-22 12:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-04-22 11:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-04-22 10:00:00+00:00,FR04014,no2,43.5,µg/m³ +Paris,FR,2019-04-22 09:00:00+00:00,FR04014,no2,44.4,µg/m³ +Paris,FR,2019-04-22 08:00:00+00:00,FR04014,no2,63.7,µg/m³ +Paris,FR,2019-04-22 07:00:00+00:00,FR04014,no2,51.4,µg/m³ +Paris,FR,2019-04-22 06:00:00+00:00,FR04014,no2,65.7,µg/m³ +Paris,FR,2019-04-22 05:00:00+00:00,FR04014,no2,69.8,µg/m³ +Paris,FR,2019-04-22 04:00:00+00:00,FR04014,no2,80.2,µg/m³ +Paris,FR,2019-04-22 03:00:00+00:00,FR04014,no2,87.9,µg/m³ +Paris,FR,2019-04-22 02:00:00+00:00,FR04014,no2,88.7,µg/m³ +Paris,FR,2019-04-22 01:00:00+00:00,FR04014,no2,99.0,µg/m³ +Paris,FR,2019-04-22 00:00:00+00:00,FR04014,no2,116.4,µg/m³ +Paris,FR,2019-04-21 23:00:00+00:00,FR04014,no2,105.2,µg/m³ +Paris,FR,2019-04-21 22:00:00+00:00,FR04014,no2,117.2,µg/m³ +Paris,FR,2019-04-21 21:00:00+00:00,FR04014,no2,101.1,µg/m³ +Paris,FR,2019-04-21 20:00:00+00:00,FR04014,no2,75.6,µg/m³ +Paris,FR,2019-04-21 19:00:00+00:00,FR04014,no2,45.6,µg/m³ +Paris,FR,2019-04-21 18:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-04-21 17:00:00+00:00,FR04014,no2,15.6,µg/m³ +Paris,FR,2019-04-21 16:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-04-21 15:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-04-21 14:00:00+00:00,FR04014,no2,9.3,µg/m³ +Paris,FR,2019-04-21 13:00:00+00:00,FR04014,no2,9.8,µg/m³ +Paris,FR,2019-04-21 12:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-04-21 11:00:00+00:00,FR04014,no2,15.7,µg/m³ +Paris,FR,2019-04-21 10:00:00+00:00,FR04014,no2,15.6,µg/m³ +Paris,FR,2019-04-21 09:00:00+00:00,FR04014,no2,21.5,µg/m³ +Paris,FR,2019-04-21 08:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-04-21 07:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-04-21 06:00:00+00:00,FR04014,no2,34.0,µg/m³ +Paris,FR,2019-04-21 05:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-04-21 04:00:00+00:00,FR04014,no2,24.9,µg/m³ +Paris,FR,2019-04-21 03:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-04-21 02:00:00+00:00,FR04014,no2,28.7,µg/m³ +Paris,FR,2019-04-21 01:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-04-21 00:00:00+00:00,FR04014,no2,40.5,µg/m³ +Paris,FR,2019-04-20 23:00:00+00:00,FR04014,no2,49.2,µg/m³ +Paris,FR,2019-04-20 22:00:00+00:00,FR04014,no2,52.8,µg/m³ +Paris,FR,2019-04-20 21:00:00+00:00,FR04014,no2,52.9,µg/m³ +Paris,FR,2019-04-20 20:00:00+00:00,FR04014,no2,39.2,µg/m³ +Paris,FR,2019-04-20 19:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-04-20 18:00:00+00:00,FR04014,no2,14.8,µg/m³ +Paris,FR,2019-04-20 17:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-04-20 16:00:00+00:00,FR04014,no2,12.7,µg/m³ +Paris,FR,2019-04-20 15:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-04-20 14:00:00+00:00,FR04014,no2,9.8,µg/m³ +Paris,FR,2019-04-20 13:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-04-20 12:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-04-20 11:00:00+00:00,FR04014,no2,28.6,µg/m³ +Paris,FR,2019-04-20 10:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-04-20 09:00:00+00:00,FR04014,no2,44.0,µg/m³ +Paris,FR,2019-04-20 08:00:00+00:00,FR04014,no2,46.3,µg/m³ +Paris,FR,2019-04-20 07:00:00+00:00,FR04014,no2,64.5,µg/m³ +Paris,FR,2019-04-20 06:00:00+00:00,FR04014,no2,67.1,µg/m³ +Paris,FR,2019-04-20 05:00:00+00:00,FR04014,no2,45.9,µg/m³ +Paris,FR,2019-04-20 04:00:00+00:00,FR04014,no2,31.5,µg/m³ +Paris,FR,2019-04-20 03:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-04-20 02:00:00+00:00,FR04014,no2,12.7,µg/m³ +Paris,FR,2019-04-20 01:00:00+00:00,FR04014,no2,14.5,µg/m³ +Paris,FR,2019-04-20 00:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-04-19 23:00:00+00:00,FR04014,no2,70.2,µg/m³ +Paris,FR,2019-04-19 22:00:00+00:00,FR04014,no2,90.4,µg/m³ +Paris,FR,2019-04-19 21:00:00+00:00,FR04014,no2,96.9,µg/m³ +Paris,FR,2019-04-19 20:00:00+00:00,FR04014,no2,78.4,µg/m³ +Paris,FR,2019-04-19 19:00:00+00:00,FR04014,no2,34.1,µg/m³ +Paris,FR,2019-04-19 18:00:00+00:00,FR04014,no2,20.2,µg/m³ +Paris,FR,2019-04-19 17:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-04-19 16:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-04-19 15:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-04-19 14:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-04-19 13:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-04-19 12:00:00+00:00,FR04014,no2,19.8,µg/m³ +Paris,FR,2019-04-19 11:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-04-19 10:00:00+00:00,FR04014,no2,51.3,µg/m³ +Paris,FR,2019-04-19 09:00:00+00:00,FR04014,no2,56.3,µg/m³ +Paris,FR,2019-04-19 08:00:00+00:00,FR04014,no2,61.4,µg/m³ +Paris,FR,2019-04-19 07:00:00+00:00,FR04014,no2,86.5,µg/m³ +Paris,FR,2019-04-19 06:00:00+00:00,FR04014,no2,89.3,µg/m³ +Paris,FR,2019-04-19 05:00:00+00:00,FR04014,no2,58.1,µg/m³ +Paris,FR,2019-04-19 04:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-04-19 03:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-04-19 02:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-04-19 01:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-04-19 00:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-04-18 23:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-04-18 22:00:00+00:00,FR04014,no2,41.2,µg/m³ +Paris,FR,2019-04-18 21:00:00+00:00,FR04014,no2,52.7,µg/m³ +Paris,FR,2019-04-18 20:00:00+00:00,FR04014,no2,43.8,µg/m³ +Paris,FR,2019-04-18 19:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-04-18 18:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-04-18 17:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-04-18 16:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-04-18 15:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-04-18 14:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-04-18 13:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-04-18 12:00:00+00:00,FR04014,no2,12.7,µg/m³ +Paris,FR,2019-04-18 11:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-04-18 10:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-04-18 09:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-04-18 08:00:00+00:00,FR04014,no2,41.9,µg/m³ +Paris,FR,2019-04-18 07:00:00+00:00,FR04014,no2,43.8,µg/m³ +Paris,FR,2019-04-18 06:00:00+00:00,FR04014,no2,47.2,µg/m³ +Paris,FR,2019-04-18 05:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-04-18 04:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-04-18 03:00:00+00:00,FR04014,no2,17.6,µg/m³ +Paris,FR,2019-04-18 02:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-04-18 01:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-04-18 00:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-04-17 23:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-04-17 22:00:00+00:00,FR04014,no2,24.7,µg/m³ +Paris,FR,2019-04-17 21:00:00+00:00,FR04014,no2,37.3,µg/m³ +Paris,FR,2019-04-17 20:00:00+00:00,FR04014,no2,41.2,µg/m³ +Paris,FR,2019-04-17 19:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-04-17 18:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-04-17 17:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-04-17 16:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-04-17 15:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-04-17 14:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-04-17 13:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-04-17 12:00:00+00:00,FR04014,no2,15.8,µg/m³ +Paris,FR,2019-04-17 11:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-04-17 10:00:00+00:00,FR04014,no2,46.9,µg/m³ +Paris,FR,2019-04-17 09:00:00+00:00,FR04014,no2,69.3,µg/m³ +Paris,FR,2019-04-17 08:00:00+00:00,FR04014,no2,72.7,µg/m³ +Paris,FR,2019-04-17 07:00:00+00:00,FR04014,no2,70.4,µg/m³ +Paris,FR,2019-04-17 06:00:00+00:00,FR04014,no2,72.9,µg/m³ +Paris,FR,2019-04-17 05:00:00+00:00,FR04014,no2,67.3,µg/m³ +Paris,FR,2019-04-17 04:00:00+00:00,FR04014,no2,65.5,µg/m³ +Paris,FR,2019-04-17 03:00:00+00:00,FR04014,no2,62.5,µg/m³ +Paris,FR,2019-04-17 02:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-04-17 01:00:00+00:00,FR04014,no2,30.7,µg/m³ +Paris,FR,2019-04-17 00:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-04-16 23:00:00+00:00,FR04014,no2,34.4,µg/m³ +Paris,FR,2019-04-16 22:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-04-16 21:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-04-16 20:00:00+00:00,FR04014,no2,28.3,µg/m³ +Paris,FR,2019-04-16 19:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-04-16 18:00:00+00:00,FR04014,no2,39.4,µg/m³ +Paris,FR,2019-04-16 17:00:00+00:00,FR04014,no2,44.0,µg/m³ +Paris,FR,2019-04-16 16:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-04-16 15:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-04-16 14:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-04-16 13:00:00+00:00,FR04014,no2,36.3,µg/m³ +Paris,FR,2019-04-16 12:00:00+00:00,FR04014,no2,40.8,µg/m³ +Paris,FR,2019-04-16 11:00:00+00:00,FR04014,no2,38.8,µg/m³ +Paris,FR,2019-04-16 10:00:00+00:00,FR04014,no2,47.1,µg/m³ +Paris,FR,2019-04-16 09:00:00+00:00,FR04014,no2,57.5,µg/m³ +Paris,FR,2019-04-16 08:00:00+00:00,FR04014,no2,58.8,µg/m³ +Paris,FR,2019-04-16 07:00:00+00:00,FR04014,no2,72.0,µg/m³ +Paris,FR,2019-04-16 06:00:00+00:00,FR04014,no2,79.0,µg/m³ +Paris,FR,2019-04-16 05:00:00+00:00,FR04014,no2,76.9,µg/m³ +Paris,FR,2019-04-16 04:00:00+00:00,FR04014,no2,60.1,µg/m³ +Paris,FR,2019-04-16 03:00:00+00:00,FR04014,no2,34.6,µg/m³ +Paris,FR,2019-04-16 02:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-04-16 01:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-04-16 00:00:00+00:00,FR04014,no2,29.7,µg/m³ +Paris,FR,2019-04-15 23:00:00+00:00,FR04014,no2,26.9,µg/m³ +Paris,FR,2019-04-15 22:00:00+00:00,FR04014,no2,29.9,µg/m³ +Paris,FR,2019-04-15 21:00:00+00:00,FR04014,no2,33.5,µg/m³ +Paris,FR,2019-04-15 20:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-04-15 19:00:00+00:00,FR04014,no2,32.4,µg/m³ +Paris,FR,2019-04-15 18:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-04-15 17:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-04-15 16:00:00+00:00,FR04014,no2,14.3,µg/m³ +Paris,FR,2019-04-15 15:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-04-15 14:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-04-15 13:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-04-15 12:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-04-15 11:00:00+00:00,FR04014,no2,13.6,µg/m³ +Paris,FR,2019-04-15 10:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-04-15 09:00:00+00:00,FR04014,no2,28.0,µg/m³ +Paris,FR,2019-04-15 08:00:00+00:00,FR04014,no2,53.9,µg/m³ +Paris,FR,2019-04-15 07:00:00+00:00,FR04014,no2,61.2,µg/m³ +Paris,FR,2019-04-15 06:00:00+00:00,FR04014,no2,67.3,µg/m³ +Paris,FR,2019-04-15 05:00:00+00:00,FR04014,no2,52.9,µg/m³ +Paris,FR,2019-04-15 04:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-04-15 03:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-04-15 02:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-04-15 01:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-04-15 00:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-04-14 23:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-04-14 22:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-04-14 21:00:00+00:00,FR04014,no2,34.4,µg/m³ +Paris,FR,2019-04-14 20:00:00+00:00,FR04014,no2,29.7,µg/m³ +Paris,FR,2019-04-14 19:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-04-14 18:00:00+00:00,FR04014,no2,21.5,µg/m³ +Paris,FR,2019-04-14 17:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-04-14 16:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-04-14 15:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-04-14 14:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-04-14 13:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-04-14 12:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-04-14 11:00:00+00:00,FR04014,no2,19.7,µg/m³ +Paris,FR,2019-04-14 10:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-04-14 09:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-04-14 08:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-04-14 07:00:00+00:00,FR04014,no2,34.1,µg/m³ +Paris,FR,2019-04-14 06:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-04-14 05:00:00+00:00,FR04014,no2,30.6,µg/m³ +Paris,FR,2019-04-14 04:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-04-14 03:00:00+00:00,FR04014,no2,33.3,µg/m³ +Paris,FR,2019-04-14 02:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-04-14 01:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-04-14 00:00:00+00:00,FR04014,no2,41.1,µg/m³ +Paris,FR,2019-04-13 23:00:00+00:00,FR04014,no2,47.8,µg/m³ +Paris,FR,2019-04-13 22:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-04-13 21:00:00+00:00,FR04014,no2,43.8,µg/m³ +Paris,FR,2019-04-13 20:00:00+00:00,FR04014,no2,38.4,µg/m³ +Paris,FR,2019-04-13 19:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-04-13 18:00:00+00:00,FR04014,no2,21.1,µg/m³ +Paris,FR,2019-04-13 17:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-04-13 16:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-04-13 15:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-04-13 14:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-04-13 13:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-04-13 12:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-04-13 11:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-04-13 10:00:00+00:00,FR04014,no2,18.3,µg/m³ +Paris,FR,2019-04-13 09:00:00+00:00,FR04014,no2,24.9,µg/m³ +Paris,FR,2019-04-13 08:00:00+00:00,FR04014,no2,35.2,µg/m³ +Paris,FR,2019-04-13 07:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-04-13 06:00:00+00:00,FR04014,no2,44.3,µg/m³ +Paris,FR,2019-04-13 05:00:00+00:00,FR04014,no2,38.7,µg/m³ +Paris,FR,2019-04-13 04:00:00+00:00,FR04014,no2,31.9,µg/m³ +Paris,FR,2019-04-13 03:00:00+00:00,FR04014,no2,35.2,µg/m³ +Paris,FR,2019-04-13 02:00:00+00:00,FR04014,no2,38.9,µg/m³ +Paris,FR,2019-04-13 01:00:00+00:00,FR04014,no2,38.9,µg/m³ +Paris,FR,2019-04-13 00:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-04-12 23:00:00+00:00,FR04014,no2,40.0,µg/m³ +Paris,FR,2019-04-12 22:00:00+00:00,FR04014,no2,42.4,µg/m³ +Paris,FR,2019-04-12 21:00:00+00:00,FR04014,no2,41.6,µg/m³ +Paris,FR,2019-04-12 20:00:00+00:00,FR04014,no2,32.8,µg/m³ +Paris,FR,2019-04-12 19:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-04-12 18:00:00+00:00,FR04014,no2,26.2,µg/m³ +Paris,FR,2019-04-12 17:00:00+00:00,FR04014,no2,25.9,µg/m³ +Paris,FR,2019-04-12 16:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-04-12 15:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-04-12 14:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-04-12 13:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-04-12 12:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-04-12 11:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-04-12 10:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-04-12 09:00:00+00:00,FR04014,no2,36.5,µg/m³ +Paris,FR,2019-04-12 08:00:00+00:00,FR04014,no2,44.3,µg/m³ +Paris,FR,2019-04-12 07:00:00+00:00,FR04014,no2,48.3,µg/m³ +Paris,FR,2019-04-12 06:00:00+00:00,FR04014,no2,52.6,µg/m³ +Paris,FR,2019-04-12 05:00:00+00:00,FR04014,no2,39.0,µg/m³ +Paris,FR,2019-04-12 04:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-04-12 03:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-04-12 02:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-04-12 01:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-04-12 00:00:00+00:00,FR04014,no2,25.7,µg/m³ +Paris,FR,2019-04-11 23:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-04-11 22:00:00+00:00,FR04014,no2,42.6,µg/m³ +Paris,FR,2019-04-11 21:00:00+00:00,FR04014,no2,40.7,µg/m³ +Paris,FR,2019-04-11 20:00:00+00:00,FR04014,no2,36.3,µg/m³ +Paris,FR,2019-04-11 19:00:00+00:00,FR04014,no2,31.4,µg/m³ +Paris,FR,2019-04-11 18:00:00+00:00,FR04014,no2,26.8,µg/m³ +Paris,FR,2019-04-11 17:00:00+00:00,FR04014,no2,20.9,µg/m³ +Paris,FR,2019-04-11 16:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-04-11 15:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-04-11 14:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-04-11 13:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-04-11 12:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-04-11 11:00:00+00:00,FR04014,no2,25.4,µg/m³ +Paris,FR,2019-04-11 10:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-04-11 09:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-04-11 08:00:00+00:00,FR04014,no2,43.2,µg/m³ +Paris,FR,2019-04-11 07:00:00+00:00,FR04014,no2,44.3,µg/m³ +Paris,FR,2019-04-11 06:00:00+00:00,FR04014,no2,45.7,µg/m³ +Paris,FR,2019-04-11 05:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-04-11 04:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-04-11 03:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-04-11 02:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-04-11 01:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-04-11 00:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-04-10 23:00:00+00:00,FR04014,no2,31.3,µg/m³ +Paris,FR,2019-04-10 22:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-04-10 21:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-04-10 20:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-04-10 19:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-04-10 18:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-04-10 17:00:00+00:00,FR04014,no2,46.0,µg/m³ +Paris,FR,2019-04-10 16:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-04-10 15:00:00+00:00,FR04014,no2,32.3,µg/m³ +Paris,FR,2019-04-10 14:00:00+00:00,FR04014,no2,26.2,µg/m³ +Paris,FR,2019-04-10 13:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-04-10 12:00:00+00:00,FR04014,no2,31.8,µg/m³ +Paris,FR,2019-04-10 11:00:00+00:00,FR04014,no2,34.4,µg/m³ +Paris,FR,2019-04-10 10:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-04-10 09:00:00+00:00,FR04014,no2,41.1,µg/m³ +Paris,FR,2019-04-10 08:00:00+00:00,FR04014,no2,45.2,µg/m³ +Paris,FR,2019-04-10 07:00:00+00:00,FR04014,no2,48.5,µg/m³ +Paris,FR,2019-04-10 06:00:00+00:00,FR04014,no2,40.6,µg/m³ +Paris,FR,2019-04-10 05:00:00+00:00,FR04014,no2,26.2,µg/m³ +Paris,FR,2019-04-10 04:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-04-10 03:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-04-10 02:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-04-10 01:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-04-10 00:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-04-09 23:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-04-09 22:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-04-09 21:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-04-09 20:00:00+00:00,FR04014,no2,39.9,µg/m³ +Paris,FR,2019-04-09 19:00:00+00:00,FR04014,no2,48.7,µg/m³ +Paris,FR,2019-04-09 18:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-04-09 17:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-04-09 16:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-04-09 15:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-04-09 14:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-04-09 13:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-04-09 12:00:00+00:00,FR04014,no2,30.6,µg/m³ +Paris,FR,2019-04-09 11:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-04-09 10:00:00+00:00,FR04014,no2,67.1,µg/m³ +Paris,FR,2019-04-09 09:00:00+00:00,FR04014,no2,66.5,µg/m³ +Paris,FR,2019-04-09 08:00:00+00:00,FR04014,no2,69.5,µg/m³ +Paris,FR,2019-04-09 07:00:00+00:00,FR04014,no2,68.0,µg/m³ +Paris,FR,2019-04-09 06:00:00+00:00,FR04014,no2,66.9,µg/m³ +Paris,FR,2019-04-09 05:00:00+00:00,FR04014,no2,59.5,µg/m³ +Paris,FR,2019-04-09 04:00:00+00:00,FR04014,no2,48.5,µg/m³ +Paris,FR,2019-04-09 03:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-04-09 02:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-04-09 01:00:00+00:00,FR04014,no2,24.4,µg/m³ +Antwerpen,BE,2019-06-17 08:00:00+00:00,BETR801,no2,41.0,µg/m³ +Antwerpen,BE,2019-06-17 07:00:00+00:00,BETR801,no2,45.0,µg/m³ +Antwerpen,BE,2019-06-17 06:00:00+00:00,BETR801,no2,43.5,µg/m³ +Antwerpen,BE,2019-06-17 05:00:00+00:00,BETR801,no2,42.5,µg/m³ +Antwerpen,BE,2019-06-17 04:00:00+00:00,BETR801,no2,39.5,µg/m³ +Antwerpen,BE,2019-06-17 03:00:00+00:00,BETR801,no2,36.0,µg/m³ +Antwerpen,BE,2019-06-17 02:00:00+00:00,BETR801,no2,35.5,µg/m³ +Antwerpen,BE,2019-06-17 01:00:00+00:00,BETR801,no2,42.0,µg/m³ +Antwerpen,BE,2019-06-16 01:00:00+00:00,BETR801,no2,42.5,µg/m³ +Antwerpen,BE,2019-06-15 01:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-06-14 09:00:00+00:00,BETR801,no2,36.5,µg/m³ +Antwerpen,BE,2019-06-13 01:00:00+00:00,BETR801,no2,28.5,µg/m³ +Antwerpen,BE,2019-06-12 01:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-06-11 01:00:00+00:00,BETR801,no2,7.5,µg/m³ +Antwerpen,BE,2019-06-10 01:00:00+00:00,BETR801,no2,18.5,µg/m³ +Antwerpen,BE,2019-06-09 01:00:00+00:00,BETR801,no2,10.0,µg/m³ +Antwerpen,BE,2019-06-05 01:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-06-01 01:00:00+00:00,BETR801,no2,52.5,µg/m³ +Antwerpen,BE,2019-05-31 01:00:00+00:00,BETR801,no2,9.0,µg/m³ +Antwerpen,BE,2019-05-30 01:00:00+00:00,BETR801,no2,7.5,µg/m³ +Antwerpen,BE,2019-05-29 01:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-05-28 01:00:00+00:00,BETR801,no2,11.0,µg/m³ +Antwerpen,BE,2019-05-27 01:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-05-26 01:00:00+00:00,BETR801,no2,53.0,µg/m³ +Antwerpen,BE,2019-05-25 01:00:00+00:00,BETR801,no2,29.0,µg/m³ +Antwerpen,BE,2019-05-24 01:00:00+00:00,BETR801,no2,74.5,µg/m³ +Antwerpen,BE,2019-05-23 01:00:00+00:00,BETR801,no2,60.5,µg/m³ +Antwerpen,BE,2019-05-22 01:00:00+00:00,BETR801,no2,20.5,µg/m³ +Antwerpen,BE,2019-05-21 01:00:00+00:00,BETR801,no2,15.5,µg/m³ +Antwerpen,BE,2019-05-20 15:00:00+00:00,BETR801,no2,25.5,µg/m³ +Antwerpen,BE,2019-05-20 14:00:00+00:00,BETR801,no2,24.5,µg/m³ +Antwerpen,BE,2019-05-20 13:00:00+00:00,BETR801,no2,32.0,µg/m³ +Antwerpen,BE,2019-05-20 12:00:00+00:00,BETR801,no2,34.5,µg/m³ +Antwerpen,BE,2019-05-20 11:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-05-20 10:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-05-20 09:00:00+00:00,BETR801,no2,30.5,µg/m³ +Antwerpen,BE,2019-05-20 08:00:00+00:00,BETR801,no2,40.0,µg/m³ +Antwerpen,BE,2019-05-20 07:00:00+00:00,BETR801,no2,38.0,µg/m³ +Antwerpen,BE,2019-05-20 06:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-20 05:00:00+00:00,BETR801,no2,20.0,µg/m³ +Antwerpen,BE,2019-05-20 04:00:00+00:00,BETR801,no2,14.0,µg/m³ +Antwerpen,BE,2019-05-20 03:00:00+00:00,BETR801,no2,9.0,µg/m³ +Antwerpen,BE,2019-05-20 02:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-05-20 01:00:00+00:00,BETR801,no2,17.0,µg/m³ +Antwerpen,BE,2019-05-20 00:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-19 23:00:00+00:00,BETR801,no2,16.5,µg/m³ +Antwerpen,BE,2019-05-19 22:00:00+00:00,BETR801,no2,18.5,µg/m³ +Antwerpen,BE,2019-05-19 21:00:00+00:00,BETR801,no2,12.5,µg/m³ +Antwerpen,BE,2019-05-19 20:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-05-19 19:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-19 18:00:00+00:00,BETR801,no2,15.5,µg/m³ +Antwerpen,BE,2019-05-19 17:00:00+00:00,BETR801,no2,18.5,µg/m³ +Antwerpen,BE,2019-05-19 16:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-05-19 15:00:00+00:00,BETR801,no2,33.0,µg/m³ +Antwerpen,BE,2019-05-19 14:00:00+00:00,BETR801,no2,23.0,µg/m³ +Antwerpen,BE,2019-05-19 13:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-19 12:00:00+00:00,BETR801,no2,16.0,µg/m³ +Antwerpen,BE,2019-05-19 11:00:00+00:00,BETR801,no2,17.0,µg/m³ +Antwerpen,BE,2019-05-19 10:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-05-19 09:00:00+00:00,BETR801,no2,16.0,µg/m³ +Antwerpen,BE,2019-05-19 08:00:00+00:00,BETR801,no2,23.5,µg/m³ +Antwerpen,BE,2019-05-19 07:00:00+00:00,BETR801,no2,30.0,µg/m³ +Antwerpen,BE,2019-05-19 06:00:00+00:00,BETR801,no2,30.5,µg/m³ +Antwerpen,BE,2019-05-19 05:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-19 04:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-05-19 03:00:00+00:00,BETR801,no2,19.0,µg/m³ +Antwerpen,BE,2019-05-19 02:00:00+00:00,BETR801,no2,19.0,µg/m³ +Antwerpen,BE,2019-05-19 01:00:00+00:00,BETR801,no2,22.5,µg/m³ +Antwerpen,BE,2019-05-19 00:00:00+00:00,BETR801,no2,23.5,µg/m³ +Antwerpen,BE,2019-05-18 23:00:00+00:00,BETR801,no2,29.5,µg/m³ +Antwerpen,BE,2019-05-18 22:00:00+00:00,BETR801,no2,34.5,µg/m³ +Antwerpen,BE,2019-05-18 21:00:00+00:00,BETR801,no2,39.0,µg/m³ +Antwerpen,BE,2019-05-18 20:00:00+00:00,BETR801,no2,40.0,µg/m³ +Antwerpen,BE,2019-05-18 19:00:00+00:00,BETR801,no2,35.5,µg/m³ +Antwerpen,BE,2019-05-18 18:00:00+00:00,BETR801,no2,35.5,µg/m³ +Antwerpen,BE,2019-05-18 01:00:00+00:00,BETR801,no2,41.5,µg/m³ +Antwerpen,BE,2019-05-16 01:00:00+00:00,BETR801,no2,28.0,µg/m³ +Antwerpen,BE,2019-05-15 02:00:00+00:00,BETR801,no2,22.5,µg/m³ +Antwerpen,BE,2019-05-15 01:00:00+00:00,BETR801,no2,25.5,µg/m³ +Antwerpen,BE,2019-05-14 02:00:00+00:00,BETR801,no2,11.5,µg/m³ +Antwerpen,BE,2019-05-14 01:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-13 02:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-13 01:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-12 02:00:00+00:00,BETR801,no2,20.0,µg/m³ +Antwerpen,BE,2019-05-12 01:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-05-11 02:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-05-11 01:00:00+00:00,BETR801,no2,26.5,µg/m³ +Antwerpen,BE,2019-05-10 02:00:00+00:00,BETR801,no2,11.5,µg/m³ +Antwerpen,BE,2019-05-10 01:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-05-09 02:00:00+00:00,BETR801,no2,20.5,µg/m³ +Antwerpen,BE,2019-05-09 01:00:00+00:00,BETR801,no2,20.0,µg/m³ +Antwerpen,BE,2019-05-08 02:00:00+00:00,BETR801,no2,20.5,µg/m³ +Antwerpen,BE,2019-05-08 01:00:00+00:00,BETR801,no2,23.0,µg/m³ +Antwerpen,BE,2019-05-07 02:00:00+00:00,BETR801,no2,45.0,µg/m³ +Antwerpen,BE,2019-05-07 01:00:00+00:00,BETR801,no2,50.5,µg/m³ +Antwerpen,BE,2019-05-06 02:00:00+00:00,BETR801,no2,27.0,µg/m³ +Antwerpen,BE,2019-05-06 01:00:00+00:00,BETR801,no2,30.0,µg/m³ +Antwerpen,BE,2019-05-05 02:00:00+00:00,BETR801,no2,13.0,µg/m³ +Antwerpen,BE,2019-05-05 01:00:00+00:00,BETR801,no2,18.0,µg/m³ +Antwerpen,BE,2019-05-04 02:00:00+00:00,BETR801,no2,9.5,µg/m³ +Antwerpen,BE,2019-05-04 01:00:00+00:00,BETR801,no2,8.5,µg/m³ +Antwerpen,BE,2019-05-03 02:00:00+00:00,BETR801,no2,25.5,µg/m³ +Antwerpen,BE,2019-05-03 01:00:00+00:00,BETR801,no2,14.0,µg/m³ +Antwerpen,BE,2019-05-02 02:00:00+00:00,BETR801,no2,36.5,µg/m³ +Antwerpen,BE,2019-05-02 01:00:00+00:00,BETR801,no2,31.0,µg/m³ +Antwerpen,BE,2019-05-01 02:00:00+00:00,BETR801,no2,12.0,µg/m³ +Antwerpen,BE,2019-05-01 01:00:00+00:00,BETR801,no2,12.5,µg/m³ +Antwerpen,BE,2019-04-30 02:00:00+00:00,BETR801,no2,9.0,µg/m³ +Antwerpen,BE,2019-04-30 01:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-04-29 02:00:00+00:00,BETR801,no2,52.5,µg/m³ +Antwerpen,BE,2019-04-29 01:00:00+00:00,BETR801,no2,72.5,µg/m³ +Antwerpen,BE,2019-04-28 02:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-04-28 01:00:00+00:00,BETR801,no2,8.5,µg/m³ +Antwerpen,BE,2019-04-27 02:00:00+00:00,BETR801,no2,14.0,µg/m³ +Antwerpen,BE,2019-04-27 01:00:00+00:00,BETR801,no2,22.0,µg/m³ +Antwerpen,BE,2019-04-26 02:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-04-26 01:00:00+00:00,BETR801,no2,25.5,µg/m³ +Antwerpen,BE,2019-04-25 02:00:00+00:00,BETR801,no2,12.0,µg/m³ +Antwerpen,BE,2019-04-25 01:00:00+00:00,BETR801,no2,13.0,µg/m³ +Antwerpen,BE,2019-04-22 01:00:00+00:00,BETR801,no2,24.5,µg/m³ +Antwerpen,BE,2019-04-21 02:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-04-21 01:00:00+00:00,BETR801,no2,18.0,µg/m³ +Antwerpen,BE,2019-04-19 01:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-04-18 02:00:00+00:00,BETR801,no2,35.0,µg/m³ +Antwerpen,BE,2019-04-17 03:00:00+00:00,BETR801,no2,38.5,µg/m³ +Antwerpen,BE,2019-04-17 02:00:00+00:00,BETR801,no2,33.0,µg/m³ +Antwerpen,BE,2019-04-17 01:00:00+00:00,BETR801,no2,33.0,µg/m³ +Antwerpen,BE,2019-04-16 02:00:00+00:00,BETR801,no2,21.5,µg/m³ +Antwerpen,BE,2019-04-16 01:00:00+00:00,BETR801,no2,27.5,µg/m³ +Antwerpen,BE,2019-04-15 15:00:00+00:00,BETR801,no2,32.0,µg/m³ +Antwerpen,BE,2019-04-15 14:00:00+00:00,BETR801,no2,28.0,µg/m³ +Antwerpen,BE,2019-04-15 13:00:00+00:00,BETR801,no2,31.0,µg/m³ +Antwerpen,BE,2019-04-15 12:00:00+00:00,BETR801,no2,29.5,µg/m³ +Antwerpen,BE,2019-04-15 11:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-04-15 10:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-04-15 09:00:00+00:00,BETR801,no2,29.5,µg/m³ +Antwerpen,BE,2019-04-15 08:00:00+00:00,BETR801,no2,43.5,µg/m³ +Antwerpen,BE,2019-04-15 07:00:00+00:00,BETR801,no2,54.0,µg/m³ +Antwerpen,BE,2019-04-15 06:00:00+00:00,BETR801,no2,64.0,µg/m³ +Antwerpen,BE,2019-04-15 05:00:00+00:00,BETR801,no2,63.0,µg/m³ +Antwerpen,BE,2019-04-15 04:00:00+00:00,BETR801,no2,49.0,µg/m³ +Antwerpen,BE,2019-04-15 03:00:00+00:00,BETR801,no2,36.5,µg/m³ +Antwerpen,BE,2019-04-15 02:00:00+00:00,BETR801,no2,32.0,µg/m³ +Antwerpen,BE,2019-04-15 01:00:00+00:00,BETR801,no2,30.5,µg/m³ +Antwerpen,BE,2019-04-12 02:00:00+00:00,BETR801,no2,22.5,µg/m³ +Antwerpen,BE,2019-04-12 01:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-04-11 02:00:00+00:00,BETR801,no2,14.0,µg/m³ +Antwerpen,BE,2019-04-11 01:00:00+00:00,BETR801,no2,13.5,µg/m³ +Antwerpen,BE,2019-04-10 02:00:00+00:00,BETR801,no2,11.5,µg/m³ +Antwerpen,BE,2019-04-10 01:00:00+00:00,BETR801,no2,13.5,µg/m³ +Antwerpen,BE,2019-04-09 13:00:00+00:00,BETR801,no2,27.5,µg/m³ +Antwerpen,BE,2019-04-09 12:00:00+00:00,BETR801,no2,30.0,µg/m³ +Antwerpen,BE,2019-04-09 11:00:00+00:00,BETR801,no2,28.5,µg/m³ +Antwerpen,BE,2019-04-09 10:00:00+00:00,BETR801,no2,33.5,µg/m³ +Antwerpen,BE,2019-04-09 09:00:00+00:00,BETR801,no2,35.0,µg/m³ +Antwerpen,BE,2019-04-09 08:00:00+00:00,BETR801,no2,39.0,µg/m³ +Antwerpen,BE,2019-04-09 07:00:00+00:00,BETR801,no2,38.5,µg/m³ +Antwerpen,BE,2019-04-09 06:00:00+00:00,BETR801,no2,50.0,µg/m³ +Antwerpen,BE,2019-04-09 05:00:00+00:00,BETR801,no2,46.5,µg/m³ +Antwerpen,BE,2019-04-09 04:00:00+00:00,BETR801,no2,34.5,µg/m³ +Antwerpen,BE,2019-04-09 03:00:00+00:00,BETR801,no2,54.5,µg/m³ +Antwerpen,BE,2019-04-09 02:00:00+00:00,BETR801,no2,53.5,µg/m³ +Antwerpen,BE,2019-04-09 01:00:00+00:00,BETR801,no2,22.5,µg/m³ +London,GB,2019-06-17 11:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 10:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 09:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 08:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-17 07:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-17 06:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-17 05:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 04:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 03:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-17 02:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-17 01:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-17 00:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-16 23:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-16 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-16 20:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-16 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-16 18:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-16 17:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-16 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-16 15:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-16 14:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-16 13:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-16 12:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-16 11:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-16 10:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-16 09:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-16 08:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-16 07:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-16 06:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-16 05:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-16 04:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-16 03:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-16 02:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-16 01:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-16 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-15 23:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-15 22:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-15 21:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-15 20:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-15 19:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-15 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-15 17:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-15 16:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-15 15:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-15 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-15 13:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-15 12:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-15 11:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-15 10:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-15 09:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-15 08:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-15 07:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-15 06:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-15 05:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-15 04:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-15 00:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-14 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 19:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 18:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-14 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 16:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-14 15:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-14 14:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-14 13:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-14 12:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-14 11:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-14 10:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-14 09:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-14 08:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-14 07:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-14 06:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-14 05:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-14 04:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-14 03:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-14 02:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-14 00:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-13 23:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 22:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 21:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 20:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-13 19:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-13 18:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-13 17:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 16:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-13 15:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-13 14:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 13:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 12:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 11:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 10:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-13 09:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 08:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 07:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-13 06:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 05:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 04:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-13 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-13 00:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-12 23:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-12 21:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-12 20:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-06-12 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-12 18:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-12 17:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-12 16:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-12 15:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-06-12 14:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-12 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-12 12:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-12 11:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-12 10:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-12 09:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-12 08:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-12 07:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-12 06:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-12 05:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-12 04:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-12 03:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-12 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-11 23:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-11 22:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-11 21:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 20:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 19:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-06-11 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-11 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-06-11 16:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-11 15:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-11 14:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-11 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-11 12:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-11 11:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 10:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 09:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-11 08:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-11 07:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-11 06:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-11 05:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-11 04:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-11 03:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-11 02:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-11 01:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-11 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-10 23:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-10 22:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-10 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-10 19:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-10 18:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-10 17:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-10 16:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-10 15:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-06-10 14:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-06-10 13:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-06-10 12:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-10 11:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-06-10 10:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-10 09:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-10 08:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-10 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-10 06:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 05:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 04:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 03:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-10 02:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-10 01:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-10 00:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-09 23:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-09 21:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-09 20:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-09 19:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-09 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-09 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-09 16:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-09 15:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-09 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-09 13:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-09 12:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-09 11:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-09 10:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-09 09:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-09 08:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-09 07:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-09 06:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-09 05:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-09 04:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-09 03:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-09 02:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-09 01:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-09 00:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-08 23:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-08 21:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-08 20:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-08 19:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-08 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-08 17:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-08 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-08 15:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-08 14:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-08 13:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-08 12:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-08 11:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-08 10:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 09:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-08 08:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-08 07:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 06:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-08 05:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 04:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 03:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-08 02:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-08 00:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-07 23:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-07 21:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-07 20:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-07 19:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-07 18:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-07 16:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-07 15:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-07 14:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-07 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-07 12:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 11:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-07 10:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 09:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-07 08:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-07 07:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 06:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-07 05:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 04:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 03:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 02:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-06 23:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-06 22:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-06 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 20:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-06 19:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 18:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 16:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-06 15:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-06 14:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-06 13:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-06 12:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-06 11:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-06 10:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-06 09:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-06 08:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-06 07:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-06 06:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-06 05:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-06 04:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-06 03:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-06 02:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-06 00:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-05 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-05 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-05 21:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 20:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 19:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 18:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 17:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 15:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-05 14:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-05 13:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-05 12:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-05 11:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-05 10:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-05 09:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-05 08:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-05 07:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-05 06:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-05 05:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-05 04:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-05 03:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-05 02:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-05 01:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-05 00:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-04 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-04 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-04 21:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-04 20:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-04 19:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-04 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-04 17:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-06-04 16:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-06-04 15:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-06-04 14:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-04 13:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-06-04 12:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-04 11:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-04 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-04 09:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-04 08:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-04 07:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-04 06:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-04 05:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-04 04:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-04 03:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-04 02:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-04 01:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-04 00:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-03 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-03 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-03 21:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-03 20:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-03 19:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-03 18:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-03 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-03 16:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-03 15:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-03 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-03 13:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-03 12:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-03 11:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-03 10:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-03 09:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-03 08:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-03 07:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-03 06:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-03 05:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-03 04:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-03 03:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-03 02:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-03 01:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-03 00:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-02 23:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-02 22:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-02 21:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-02 20:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 19:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-02 18:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-02 17:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 15:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-02 14:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-02 13:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-02 12:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-02 11:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-02 10:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-02 09:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 08:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-06-02 07:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 06:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 05:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-06-02 04:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-06-02 03:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-02 02:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-02 01:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-06-02 00:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-06-01 23:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-06-01 22:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-06-01 21:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-06-01 20:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-06-01 19:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-06-01 18:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-01 17:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-01 16:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-01 15:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-01 14:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-01 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-01 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-01 11:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-01 10:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-01 09:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-01 08:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-01 07:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-01 06:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-01 05:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-01 04:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-01 03:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-01 02:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-01 01:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-01 00:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-31 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-31 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-31 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-31 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-31 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 16:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 15:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-31 14:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-31 13:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-31 12:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-31 11:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-31 10:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-31 09:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-31 08:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-05-31 07:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-31 06:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-05-31 05:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-31 04:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-31 03:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-05-31 02:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-05-31 01:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-31 00:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-30 23:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-30 22:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-30 21:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-30 20:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-30 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-30 18:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-30 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-30 16:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-30 15:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-30 14:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-30 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-30 12:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-30 11:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-05-30 10:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-30 09:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-30 08:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-05-30 07:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-05-30 06:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 05:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 04:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 03:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 02:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 01:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-05-30 00:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-05-29 23:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 22:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 21:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-05-29 20:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-05-29 19:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 18:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 17:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 16:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-05-29 15:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 14:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-29 13:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-05-29 12:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-29 11:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-29 10:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-29 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-29 08:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-29 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-29 06:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-29 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-29 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-29 03:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-29 02:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-29 01:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-29 00:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-28 23:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-28 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-28 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 19:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 17:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-28 16:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-28 15:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-28 14:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-28 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-28 11:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-28 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-28 09:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-28 08:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-28 07:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-28 06:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-28 05:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-28 04:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-28 03:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-28 02:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-28 01:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-28 00:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-27 23:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 22:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 21:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-27 20:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-27 19:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 17:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 16:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 15:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 14:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-27 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-27 12:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-27 11:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-27 10:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-27 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 08:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 07:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 06:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 03:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-27 02:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-27 01:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-27 00:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 21:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-26 20:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-26 19:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 18:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-26 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 16:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-26 15:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 14:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-26 13:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-26 12:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-26 11:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-26 10:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-26 09:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-26 08:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-26 07:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-26 06:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-26 05:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-26 04:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-26 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 01:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-26 00:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-25 23:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-25 22:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-25 21:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-25 20:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-25 19:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-25 18:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-25 17:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-25 16:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-25 15:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-25 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-25 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-25 12:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-25 11:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-25 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-25 09:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-25 08:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-25 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-25 06:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-25 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-25 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-25 03:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-25 02:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-25 01:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-25 00:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-24 23:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-24 22:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-24 21:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-24 20:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-24 19:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-24 18:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-24 17:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-24 16:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-05-24 15:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-24 14:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-24 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-24 12:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-24 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-24 10:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-24 09:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-24 08:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-24 07:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-24 06:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-24 05:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-24 04:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-24 03:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-24 02:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-24 00:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-23 23:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-23 22:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-23 21:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-23 20:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-05-23 19:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-05-23 18:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-05-23 17:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-05-23 16:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-05-23 15:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-23 14:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-23 13:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-23 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-23 11:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-23 10:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-23 09:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-23 08:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-23 07:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-23 06:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-23 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-23 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-23 03:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-23 02:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-23 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-23 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-22 23:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-22 22:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-22 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-22 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-22 18:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-22 17:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-22 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-22 15:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-22 14:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-22 13:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-22 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-22 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-22 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 09:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 08:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 07:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-22 06:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-22 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-22 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-22 03:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-22 02:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-22 01:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-22 00:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-21 23:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 22:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 21:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 20:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-21 19:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-21 18:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-21 17:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-21 16:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-21 15:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-21 14:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 13:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-21 12:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 10:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-21 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-21 08:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-21 07:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-21 06:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-21 05:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-21 04:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-21 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 01:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-21 00:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-20 23:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-20 22:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-20 21:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-20 20:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 19:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 18:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-20 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-20 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 15:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 14:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 13:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 08:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 07:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-20 06:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-20 05:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-20 04:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-20 03:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 02:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 01:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 00:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-19 23:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 22:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 21:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 20:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 19:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-19 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-19 17:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-19 16:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-19 15:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-19 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-19 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-19 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-19 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-19 10:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-19 09:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-19 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-19 07:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-19 06:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-19 05:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 04:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 03:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 02:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 01:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 00:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-18 23:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-18 22:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-18 21:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-18 20:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-18 19:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-18 18:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-18 17:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-18 16:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-18 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-18 14:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-18 13:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 12:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-18 11:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-18 10:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-18 09:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-18 08:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-18 07:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 06:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-18 05:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 04:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 03:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-18 02:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-18 01:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-18 00:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-17 23:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-17 22:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-17 21:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-17 20:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-17 19:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-17 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-17 17:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-17 16:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 15:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-17 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-17 13:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-17 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 11:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 10:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-17 08:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-17 07:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-17 06:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-17 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 03:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 02:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-17 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-16 23:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 22:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 19:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-16 18:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-16 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-16 15:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 14:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-16 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-16 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-16 11:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-16 09:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-16 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-16 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 05:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 04:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 03:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-16 02:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-16 01:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 00:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 23:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 22:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 21:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-15 20:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-15 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 18:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-15 17:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 16:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-15 15:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-15 14:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-15 13:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-15 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-15 11:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-15 10:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-15 09:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-15 08:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-15 07:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 05:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-15 04:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-15 03:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-15 02:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-15 00:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-14 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 20:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-14 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-14 18:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 17:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-14 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-14 15:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-14 14:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 13:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 11:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-14 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-14 09:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-14 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-14 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-14 05:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 04:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-14 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-13 23:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 22:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 20:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 19:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 18:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-13 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-13 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-13 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-13 14:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-13 13:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-13 12:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-13 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-13 10:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-13 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-13 08:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 07:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-13 06:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-13 05:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-13 04:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-13 03:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-13 02:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-13 01:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-13 00:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 23:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 22:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 21:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-12 20:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 19:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-12 16:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-12 15:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 14:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-12 13:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-12 12:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-12 11:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-12 10:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-12 09:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-12 08:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-12 07:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-12 06:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-12 05:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-12 04:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-12 03:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-12 02:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-12 01:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-12 00:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-11 23:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-11 22:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-11 21:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-11 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-11 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-11 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-11 17:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-11 16:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-11 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-11 09:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-11 08:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-11 07:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-11 06:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-11 05:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-11 04:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-11 03:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-11 02:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-11 01:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-11 00:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-10 23:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-10 22:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-10 21:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 19:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 18:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-10 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-10 16:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-10 15:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-10 14:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-10 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-10 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-10 11:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-10 09:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-10 08:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-10 07:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-10 06:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-10 05:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-10 04:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-10 03:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-10 02:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-10 01:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-05-10 00:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-05-09 23:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-05-09 22:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-05-09 21:00:00+00:00,London Westminster,no2,65.0,µg/m³ +London,GB,2019-05-09 20:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-05-09 19:00:00+00:00,London Westminster,no2,62.0,µg/m³ +London,GB,2019-05-09 18:00:00+00:00,London Westminster,no2,58.0,µg/m³ +London,GB,2019-05-09 17:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-05-09 16:00:00+00:00,London Westminster,no2,67.0,µg/m³ +London,GB,2019-05-09 15:00:00+00:00,London Westminster,no2,97.0,µg/m³ +London,GB,2019-05-09 14:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-09 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-09 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-09 11:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-09 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-09 09:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-09 08:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-09 07:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 05:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 04:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 03:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-09 02:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-09 00:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-08 23:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-08 21:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-08 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-08 19:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-08 18:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-08 17:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-08 16:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-08 15:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-08 14:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-08 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-08 12:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-08 11:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-08 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-08 09:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-08 08:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-08 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-08 06:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-08 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-08 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-08 03:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-08 02:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-08 01:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-08 00:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-07 23:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-07 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-07 20:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-07 19:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 17:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-07 16:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 15:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 14:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-07 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 12:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-07 11:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-07 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-07 09:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-07 08:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-07 07:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-07 06:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-07 04:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-07 03:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-07 02:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-07 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-06 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-06 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-06 21:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-06 20:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-06 19:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-06 18:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-06 17:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-06 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-06 15:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-06 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-06 13:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-06 12:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-06 11:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-06 10:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-06 09:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-06 08:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-06 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-06 06:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-06 05:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-06 04:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-06 03:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-06 02:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-06 01:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-06 00:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-05 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-05 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-05 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-05 20:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-05 19:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-05 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-05 17:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-05 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-05 15:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-05 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-05 13:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-05 12:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-05 11:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-05 10:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-05 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-05 08:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-05 07:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-05 06:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-05 05:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-05 04:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-05 03:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-05 02:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-05 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-05 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-04 23:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-04 22:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-04 21:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-04 20:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-04 19:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-04 18:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-04 17:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-04 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-04 15:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-04 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-04 13:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-04 12:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-04 11:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-04 10:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-04 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-04 08:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-04 07:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-04 06:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-04 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-04 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-04 03:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-04 02:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-04 01:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-04 00:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-03 23:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-03 22:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-03 21:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-03 20:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-03 19:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-03 18:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-03 17:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-03 16:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-03 15:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-03 14:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-03 13:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-03 12:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-03 11:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-05-03 10:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-03 09:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-03 08:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-03 07:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-03 06:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-03 05:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-03 04:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-03 03:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-03 02:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-03 01:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-03 00:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-02 23:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-02 22:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-02 21:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-05-02 20:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-05-02 19:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-02 18:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-02 17:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-02 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-02 15:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-02 14:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-02 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-02 12:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-02 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-02 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-02 09:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-02 08:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-02 07:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-02 06:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-02 05:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-02 04:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-02 03:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-02 02:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-02 01:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-02 00:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-01 23:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-01 22:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-01 21:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-01 20:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-01 19:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-01 18:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-01 17:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-01 16:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-01 15:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-01 14:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-01 13:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-01 12:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-01 11:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-01 10:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-01 09:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-01 08:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-01 07:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-01 06:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-01 05:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-01 04:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-01 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-01 00:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-30 23:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-30 22:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-30 21:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-30 20:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-04-30 19:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-30 18:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-30 17:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-30 16:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-30 15:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-30 14:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-30 13:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-30 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-30 11:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-30 10:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-30 09:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-30 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-30 07:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-30 06:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-30 05:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-30 04:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-30 03:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-30 02:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-30 01:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-30 00:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-29 23:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-29 22:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-29 21:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-29 20:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-29 19:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-29 18:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-29 17:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-29 16:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-29 15:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-29 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-29 13:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-29 12:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-29 11:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-04-29 10:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-29 09:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-29 08:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-29 07:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-29 06:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-29 05:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-29 04:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-29 03:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-29 02:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-29 01:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-29 00:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-28 23:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-28 22:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-28 21:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-28 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-28 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-28 18:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-28 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-28 16:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-28 15:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-28 14:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-28 13:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-28 12:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-28 11:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-28 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-28 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-04-27 13:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-04-27 12:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-27 11:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-04-27 10:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-04-27 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-04-27 08:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-04-27 07:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-04-27 06:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-04-27 05:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-04-27 04:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-04-27 03:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-04-27 02:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-04-27 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-04-26 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-04-26 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-04-26 21:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-04-26 20:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-26 19:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-26 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-04-26 17:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-04-26 16:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-04-26 15:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-26 14:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-26 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-04-26 12:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-26 11:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-04-26 10:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-26 09:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-26 08:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-26 07:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-26 06:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-26 05:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-26 04:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-26 03:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-26 02:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-26 01:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-26 00:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-25 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-25 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-25 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-25 20:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-25 19:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-04-25 18:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-25 17:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-25 16:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-25 15:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-25 14:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-25 13:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-04-25 12:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-25 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-25 10:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-25 09:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-25 08:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-25 07:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-25 06:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-25 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-25 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-25 03:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-25 02:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-25 00:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-24 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-24 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-24 21:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-24 20:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-24 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-24 18:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-24 17:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-24 16:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-24 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-24 14:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-24 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-24 12:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-24 11:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-24 10:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-24 09:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-04-24 08:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-24 07:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-24 06:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-24 05:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-24 04:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-24 03:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-04-24 02:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-04-24 00:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-23 23:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-23 22:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-23 21:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-23 20:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-23 19:00:00+00:00,London Westminster,no2,48.0,µg/m³ +London,GB,2019-04-23 18:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-23 17:00:00+00:00,London Westminster,no2,62.0,µg/m³ +London,GB,2019-04-23 16:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-23 15:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-23 14:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-23 13:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-23 12:00:00+00:00,London Westminster,no2,67.0,µg/m³ +London,GB,2019-04-23 11:00:00+00:00,London Westminster,no2,67.0,µg/m³ +London,GB,2019-04-23 10:00:00+00:00,London Westminster,no2,63.0,µg/m³ +London,GB,2019-04-23 09:00:00+00:00,London Westminster,no2,61.0,µg/m³ +London,GB,2019-04-23 08:00:00+00:00,London Westminster,no2,63.0,µg/m³ +London,GB,2019-04-23 07:00:00+00:00,London Westminster,no2,62.0,µg/m³ +London,GB,2019-04-23 06:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-23 05:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-23 04:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-23 03:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-23 02:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-23 01:00:00+00:00,London Westminster,no2,75.0,µg/m³ +London,GB,2019-04-23 00:00:00+00:00,London Westminster,no2,75.0,µg/m³ +London,GB,2019-04-22 23:00:00+00:00,London Westminster,no2,84.0,µg/m³ +London,GB,2019-04-22 22:00:00+00:00,London Westminster,no2,84.0,µg/m³ +London,GB,2019-04-22 21:00:00+00:00,London Westminster,no2,73.0,µg/m³ +London,GB,2019-04-22 20:00:00+00:00,London Westminster,no2,66.0,µg/m³ +London,GB,2019-04-22 19:00:00+00:00,London Westminster,no2,66.0,µg/m³ +London,GB,2019-04-22 18:00:00+00:00,London Westminster,no2,64.0,µg/m³ +London,GB,2019-04-22 17:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-22 16:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-22 15:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-22 14:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-22 13:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-22 12:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-22 11:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-22 10:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-22 09:00:00+00:00,London Westminster,no2,48.0,µg/m³ +London,GB,2019-04-22 08:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-22 07:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-22 06:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-04-22 05:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-22 04:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-22 03:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-22 02:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-22 01:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-22 00:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-21 23:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-21 22:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-21 21:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-21 20:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-21 19:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-21 18:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-21 17:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-21 16:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-21 15:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-21 14:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-21 13:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-21 12:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-21 11:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-21 10:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-21 09:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-04-21 08:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-21 07:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-21 06:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-21 05:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-21 04:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-21 03:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-21 02:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-21 01:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-21 00:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-20 23:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-20 22:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-20 21:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-20 20:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-20 19:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-20 18:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-20 17:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-20 16:00:00+00:00,London Westminster,no2,48.0,µg/m³ +London,GB,2019-04-20 15:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-20 14:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-20 13:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-20 12:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-20 11:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-20 10:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-20 09:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-20 08:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-20 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-20 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-20 05:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-20 04:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-20 03:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-04-20 02:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-04-20 01:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-04-20 00:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-04-19 23:00:00+00:00,London Westminster,no2,77.0,µg/m³ +London,GB,2019-04-19 22:00:00+00:00,London Westminster,no2,77.0,µg/m³ +London,GB,2019-04-19 21:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-19 20:00:00+00:00,London Westminster,no2,58.0,µg/m³ +London,GB,2019-04-19 19:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-19 18:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-19 17:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-19 16:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-19 15:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-19 14:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-19 13:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-19 12:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-19 11:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-19 10:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-04-19 09:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-19 08:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-19 07:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-19 06:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-04-19 05:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-04-19 04:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-04-19 03:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-19 02:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-19 00:00:00+00:00,London Westminster,no2,58.0,µg/m³ +London,GB,2019-04-18 23:00:00+00:00,London Westminster,no2,61.0,µg/m³ +London,GB,2019-04-18 22:00:00+00:00,London Westminster,no2,61.0,µg/m³ +London,GB,2019-04-18 21:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-04-18 20:00:00+00:00,London Westminster,no2,69.0,µg/m³ +London,GB,2019-04-18 19:00:00+00:00,London Westminster,no2,63.0,µg/m³ +London,GB,2019-04-18 18:00:00+00:00,London Westminster,no2,63.0,µg/m³ +London,GB,2019-04-18 17:00:00+00:00,London Westminster,no2,56.0,µg/m³ +London,GB,2019-04-18 16:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-18 15:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-18 14:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-18 13:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-18 12:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-18 11:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-18 10:00:00+00:00,London Westminster,no2,56.0,µg/m³ +London,GB,2019-04-18 09:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-18 08:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-18 07:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-18 06:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-18 05:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-18 04:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-18 03:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-18 02:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-18 01:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-18 00:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-17 23:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-17 22:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-17 21:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-17 20:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-04-17 19:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-17 18:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-17 17:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-17 16:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-17 15:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-17 14:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-04-17 13:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-17 12:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-04-17 11:00:00+00:00,London Westminster,no2,67.0,µg/m³ +London,GB,2019-04-17 10:00:00+00:00,London Westminster,no2,56.0,µg/m³ +London,GB,2019-04-17 09:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-17 08:00:00+00:00,London Westminster,no2,48.0,µg/m³ +London,GB,2019-04-17 07:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-17 06:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-17 05:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-17 04:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-17 03:00:00+00:00,London Westminster,no2,72.0,µg/m³ +London,GB,2019-04-17 02:00:00+00:00,London Westminster,no2,72.0,µg/m³ +London,GB,2019-04-17 00:00:00+00:00,London Westminster,no2,71.0,µg/m³ +London,GB,2019-04-16 23:00:00+00:00,London Westminster,no2,81.0,µg/m³ +London,GB,2019-04-16 22:00:00+00:00,London Westminster,no2,81.0,µg/m³ +London,GB,2019-04-16 21:00:00+00:00,London Westminster,no2,84.0,µg/m³ +London,GB,2019-04-16 20:00:00+00:00,London Westminster,no2,83.0,µg/m³ +London,GB,2019-04-16 19:00:00+00:00,London Westminster,no2,76.0,µg/m³ +London,GB,2019-04-16 18:00:00+00:00,London Westminster,no2,70.0,µg/m³ +London,GB,2019-04-16 17:00:00+00:00,London Westminster,no2,65.0,µg/m³ +London,GB,2019-04-16 15:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-16 14:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-16 13:00:00+00:00,London Westminster,no2,63.0,µg/m³ +London,GB,2019-04-16 12:00:00+00:00,London Westminster,no2,75.0,µg/m³ +London,GB,2019-04-16 11:00:00+00:00,London Westminster,no2,79.0,µg/m³ +London,GB,2019-04-16 10:00:00+00:00,London Westminster,no2,70.0,µg/m³ +London,GB,2019-04-16 09:00:00+00:00,London Westminster,no2,66.0,µg/m³ +London,GB,2019-04-16 08:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-04-16 07:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-16 06:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-04-16 05:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-16 04:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-16 03:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-16 02:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-16 00:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-15 23:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-15 22:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-15 21:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-15 20:00:00+00:00,London Westminster,no2,48.0,µg/m³ +London,GB,2019-04-15 19:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-15 18:00:00+00:00,London Westminster,no2,48.0,µg/m³ +London,GB,2019-04-15 17:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-15 16:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-15 15:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-15 14:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-15 13:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-15 12:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-15 11:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-15 10:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-15 09:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-15 08:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-15 07:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-15 06:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-15 05:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-15 04:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-15 03:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-15 02:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-15 01:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-15 00:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-14 23:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-14 22:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-14 21:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-14 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-14 19:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-14 18:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-14 17:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-14 16:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-04-14 15:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-04-14 14:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-04-14 13:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-14 12:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-14 11:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-14 10:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-14 09:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-14 08:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-14 07:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-14 06:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-04-14 05:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-14 04:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-14 03:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-14 02:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-14 01:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-14 00:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-13 23:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-13 22:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-13 21:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-13 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 19:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-13 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-04-13 17:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-13 16:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-13 15:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 14:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-13 12:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 11:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-13 10:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-13 09:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-13 08:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-13 07:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-04-13 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-13 05:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 04:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 03:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-13 02:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-13 01:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-13 00:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-12 23:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-12 22:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-12 21:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-12 20:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-04-12 19:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-12 18:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-12 17:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-12 16:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-12 15:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-12 14:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-12 13:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-12 12:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-12 11:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-12 10:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-12 09:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-12 08:00:00+00:00,London Westminster,no2,57.0,µg/m³ +London,GB,2019-04-12 07:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-12 06:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-12 05:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-12 04:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-12 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-12 00:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-11 23:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-11 22:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-11 21:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-11 20:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-11 19:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-04-11 18:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-04-11 17:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-11 16:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-11 15:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-11 14:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-11 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-04-11 12:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-11 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-11 10:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-11 09:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-11 08:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-11 07:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-04-11 06:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-04-11 05:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-11 04:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-11 03:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-11 02:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-04-11 00:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-10 23:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-10 22:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-10 21:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-10 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-10 19:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-04-10 18:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-10 17:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-10 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-04-10 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-10 14:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-04-10 13:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-04-10 12:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-10 11:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-04-10 10:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-04-10 09:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-04-10 08:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-10 07:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-10 06:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-10 05:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-10 04:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-04-10 03:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-10 02:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-04-10 01:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-10 00:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-04-09 23:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-09 22:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-04-09 21:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-09 20:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-04-09 19:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-09 18:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-04-09 17:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-04-09 16:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-04-09 15:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-04-09 14:00:00+00:00,London Westminster,no2,58.0,µg/m³ +London,GB,2019-04-09 13:00:00+00:00,London Westminster,no2,56.0,µg/m³ +London,GB,2019-04-09 12:00:00+00:00,London Westminster,no2,55.0,µg/m³ +London,GB,2019-04-09 11:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-09 10:00:00+00:00,London Westminster,no2,50.0,µg/m³ +London,GB,2019-04-09 09:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-04-09 08:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-04-09 07:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-04-09 06:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-09 05:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-09 04:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-04-09 03:00:00+00:00,London Westminster,no2,67.0,µg/m³ +London,GB,2019-04-09 02:00:00+00:00,London Westminster,no2,67.0,µg/m³ diff --git a/doc/data/air_quality_no2.csv b/doc/data/air_quality_no2.csv new file mode 100644 index 0000000000000..7fa879f7c7e78 --- /dev/null +++ b/doc/data/air_quality_no2.csv @@ -0,0 +1,1036 @@ +datetime,station_antwerp,station_paris,station_london +2019-05-07 02:00:00,,,23.0 +2019-05-07 03:00:00,50.5,25.0,19.0 +2019-05-07 04:00:00,45.0,27.7,19.0 +2019-05-07 05:00:00,,50.4,16.0 +2019-05-07 06:00:00,,61.9, +2019-05-07 07:00:00,,72.4,26.0 +2019-05-07 08:00:00,,77.7,32.0 +2019-05-07 09:00:00,,67.9,32.0 +2019-05-07 10:00:00,,56.0,28.0 +2019-05-07 11:00:00,,34.5,21.0 +2019-05-07 12:00:00,,20.1,21.0 +2019-05-07 13:00:00,,13.0,18.0 +2019-05-07 14:00:00,,10.6,20.0 +2019-05-07 15:00:00,,13.2,18.0 +2019-05-07 16:00:00,,11.0,20.0 +2019-05-07 17:00:00,,11.7,20.0 +2019-05-07 18:00:00,,18.2,21.0 +2019-05-07 19:00:00,,22.3,20.0 +2019-05-07 20:00:00,,21.4,20.0 +2019-05-07 21:00:00,,26.8,24.0 +2019-05-07 22:00:00,,36.2,24.0 +2019-05-07 23:00:00,,33.9, +2019-05-08 00:00:00,,35.8,24.0 +2019-05-08 01:00:00,,34.0,19.0 +2019-05-08 02:00:00,,22.1,19.0 +2019-05-08 03:00:00,23.0,19.6,20.0 +2019-05-08 04:00:00,20.5,15.3,20.0 +2019-05-08 05:00:00,,13.5,19.0 +2019-05-08 06:00:00,,15.5,19.0 +2019-05-08 07:00:00,,19.3,29.0 +2019-05-08 08:00:00,,21.7,34.0 +2019-05-08 09:00:00,,19.5,36.0 +2019-05-08 10:00:00,,17.0,33.0 +2019-05-08 11:00:00,,19.7,28.0 +2019-05-08 12:00:00,,33.4,27.0 +2019-05-08 13:00:00,,21.4,26.0 +2019-05-08 14:00:00,,15.1,26.0 +2019-05-08 15:00:00,,14.3,24.0 +2019-05-08 16:00:00,,25.3,27.0 +2019-05-08 17:00:00,,26.0,28.0 +2019-05-08 18:00:00,,38.6,31.0 +2019-05-08 19:00:00,,29.3,40.0 +2019-05-08 20:00:00,,27.8,25.0 +2019-05-08 21:00:00,,41.3,29.0 +2019-05-08 22:00:00,,38.3,26.0 +2019-05-08 23:00:00,,48.9, +2019-05-09 00:00:00,,32.2,25.0 +2019-05-09 01:00:00,,25.2,30.0 +2019-05-09 02:00:00,,14.7, +2019-05-09 03:00:00,20.0,10.6,31.0 +2019-05-09 04:00:00,20.5,10.0,31.0 +2019-05-09 05:00:00,,10.4,33.0 +2019-05-09 06:00:00,,15.3,33.0 +2019-05-09 07:00:00,,34.5,33.0 +2019-05-09 08:00:00,,50.7,33.0 +2019-05-09 09:00:00,,49.0,35.0 +2019-05-09 10:00:00,,32.2,36.0 +2019-05-09 11:00:00,,32.3,28.0 +2019-05-09 12:00:00,,43.1,27.0 +2019-05-09 13:00:00,,34.2,30.0 +2019-05-09 14:00:00,,35.1,27.0 +2019-05-09 15:00:00,,21.3,34.0 +2019-05-09 16:00:00,,24.6,97.0 +2019-05-09 17:00:00,,23.9,67.0 +2019-05-09 18:00:00,,27.0,60.0 +2019-05-09 19:00:00,,29.9,58.0 +2019-05-09 20:00:00,,24.4,62.0 +2019-05-09 21:00:00,,23.8,59.0 +2019-05-09 22:00:00,,29.2,65.0 +2019-05-09 23:00:00,,34.5,59.0 +2019-05-10 00:00:00,,29.7,59.0 +2019-05-10 01:00:00,,26.7,52.0 +2019-05-10 02:00:00,,22.7,52.0 +2019-05-10 03:00:00,10.5,19.1,41.0 +2019-05-10 04:00:00,11.5,14.1,41.0 +2019-05-10 05:00:00,,15.0,40.0 +2019-05-10 06:00:00,,20.5,40.0 +2019-05-10 07:00:00,,37.8,39.0 +2019-05-10 08:00:00,,47.4,36.0 +2019-05-10 09:00:00,,57.3,39.0 +2019-05-10 10:00:00,,60.7,34.0 +2019-05-10 11:00:00,,53.4,31.0 +2019-05-10 12:00:00,,35.1,29.0 +2019-05-10 13:00:00,,23.2,28.0 +2019-05-10 14:00:00,,25.3,26.0 +2019-05-10 15:00:00,,22.0,25.0 +2019-05-10 16:00:00,,29.3,25.0 +2019-05-10 17:00:00,,29.6,24.0 +2019-05-10 18:00:00,,30.8,26.0 +2019-05-10 19:00:00,,37.8,26.0 +2019-05-10 20:00:00,,33.4,29.0 +2019-05-10 21:00:00,,39.3,29.0 +2019-05-10 22:00:00,,43.6,29.0 +2019-05-10 23:00:00,,37.0,31.0 +2019-05-11 00:00:00,,28.1,31.0 +2019-05-11 01:00:00,,26.0,27.0 +2019-05-11 02:00:00,,24.8,27.0 +2019-05-11 03:00:00,26.5,15.5,32.0 +2019-05-11 04:00:00,21.0,14.9,32.0 +2019-05-11 05:00:00,,,35.0 +2019-05-11 06:00:00,,,35.0 +2019-05-11 07:00:00,,,30.0 +2019-05-11 08:00:00,,28.9,30.0 +2019-05-11 09:00:00,,29.0,27.0 +2019-05-11 10:00:00,,32.1,30.0 +2019-05-11 11:00:00,,35.7, +2019-05-11 12:00:00,,36.8, +2019-05-11 13:00:00,,33.2, +2019-05-11 14:00:00,,30.2, +2019-05-11 15:00:00,,30.8, +2019-05-11 16:00:00,,17.8,28.0 +2019-05-11 17:00:00,,18.0,26.0 +2019-05-11 18:00:00,,19.5,28.0 +2019-05-11 19:00:00,,32.0,31.0 +2019-05-11 20:00:00,,33.1,33.0 +2019-05-11 21:00:00,,31.2,33.0 +2019-05-11 22:00:00,,24.2,34.0 +2019-05-11 23:00:00,,21.1,37.0 +2019-05-12 00:00:00,,27.7,37.0 +2019-05-12 01:00:00,,26.4,35.0 +2019-05-12 02:00:00,,22.8,35.0 +2019-05-12 03:00:00,17.5,19.2,38.0 +2019-05-12 04:00:00,20.0,17.2,38.0 +2019-05-12 05:00:00,,16.0,36.0 +2019-05-12 06:00:00,,16.2,36.0 +2019-05-12 07:00:00,,19.2,38.0 +2019-05-12 08:00:00,,20.1,44.0 +2019-05-12 09:00:00,,15.9,32.0 +2019-05-12 10:00:00,,14.6,26.0 +2019-05-12 11:00:00,,11.7,26.0 +2019-05-12 12:00:00,,11.4,21.0 +2019-05-12 13:00:00,,11.4,20.0 +2019-05-12 14:00:00,,10.9,19.0 +2019-05-12 15:00:00,,8.7,21.0 +2019-05-12 16:00:00,,9.1,22.0 +2019-05-12 17:00:00,,9.6,23.0 +2019-05-12 18:00:00,,11.7,24.0 +2019-05-12 19:00:00,,13.9,22.0 +2019-05-12 20:00:00,,18.2,22.0 +2019-05-12 21:00:00,,19.5,22.0 +2019-05-12 22:00:00,,24.1,21.0 +2019-05-12 23:00:00,,34.2,22.0 +2019-05-13 00:00:00,,46.5,22.0 +2019-05-13 01:00:00,,32.5,22.0 +2019-05-13 02:00:00,,25.0,22.0 +2019-05-13 03:00:00,14.5,18.9,24.0 +2019-05-13 04:00:00,14.5,18.5,24.0 +2019-05-13 05:00:00,,18.9,33.0 +2019-05-13 06:00:00,,25.1,33.0 +2019-05-13 07:00:00,,38.3,39.0 +2019-05-13 08:00:00,,45.2,39.0 +2019-05-13 09:00:00,,41.0,31.0 +2019-05-13 10:00:00,,32.1,29.0 +2019-05-13 11:00:00,,20.6,27.0 +2019-05-13 12:00:00,,12.8,26.0 +2019-05-13 13:00:00,,9.6,24.0 +2019-05-13 14:00:00,,9.2,25.0 +2019-05-13 15:00:00,,10.1,26.0 +2019-05-13 16:00:00,,10.7,28.0 +2019-05-13 17:00:00,,10.6,29.0 +2019-05-13 18:00:00,,12.1,30.0 +2019-05-13 19:00:00,,13.0,30.0 +2019-05-13 20:00:00,,15.5,31.0 +2019-05-13 21:00:00,,23.9,31.0 +2019-05-13 22:00:00,,28.3,31.0 +2019-05-13 23:00:00,,30.4,31.0 +2019-05-14 00:00:00,,27.3,31.0 +2019-05-14 01:00:00,,22.8,23.0 +2019-05-14 02:00:00,,20.9,23.0 +2019-05-14 03:00:00,14.5,19.1,26.0 +2019-05-14 04:00:00,11.5,19.0,26.0 +2019-05-14 05:00:00,,22.1,30.0 +2019-05-14 06:00:00,,31.6,30.0 +2019-05-14 07:00:00,,38.6,33.0 +2019-05-14 08:00:00,,46.1,34.0 +2019-05-14 09:00:00,,41.3,33.0 +2019-05-14 10:00:00,,28.8,30.0 +2019-05-14 11:00:00,,19.0,31.0 +2019-05-14 12:00:00,,12.9,27.0 +2019-05-14 13:00:00,,11.3,25.0 +2019-05-14 14:00:00,,10.2,25.0 +2019-05-14 15:00:00,,11.0,25.0 +2019-05-14 16:00:00,,15.2,29.0 +2019-05-14 17:00:00,,13.4,32.0 +2019-05-14 18:00:00,,15.3,33.0 +2019-05-14 19:00:00,,17.7,30.0 +2019-05-14 20:00:00,,17.9,28.0 +2019-05-14 21:00:00,,23.3,27.0 +2019-05-14 22:00:00,,28.4,25.0 +2019-05-14 23:00:00,,29.0,26.0 +2019-05-15 00:00:00,,30.9,26.0 +2019-05-15 01:00:00,,24.3,22.0 +2019-05-15 02:00:00,,18.8, +2019-05-15 03:00:00,25.5,17.2,22.0 +2019-05-15 04:00:00,22.5,16.8,22.0 +2019-05-15 05:00:00,,17.9,25.0 +2019-05-15 06:00:00,,28.9,25.0 +2019-05-15 07:00:00,,46.5,33.0 +2019-05-15 08:00:00,,48.1,33.0 +2019-05-15 09:00:00,,32.1,34.0 +2019-05-15 10:00:00,,25.7,35.0 +2019-05-15 11:00:00,,0.0,36.0 +2019-05-15 12:00:00,,0.0,35.0 +2019-05-15 13:00:00,,0.0,30.0 +2019-05-15 14:00:00,,9.4,31.0 +2019-05-15 15:00:00,,10.0,30.0 +2019-05-15 16:00:00,,11.9,38.0 +2019-05-15 17:00:00,,12.9,38.0 +2019-05-15 18:00:00,,12.2,33.0 +2019-05-15 19:00:00,,12.9,35.0 +2019-05-15 20:00:00,,16.5,33.0 +2019-05-15 21:00:00,,20.3,31.0 +2019-05-15 22:00:00,,30.1,32.0 +2019-05-15 23:00:00,,36.0,33.0 +2019-05-16 00:00:00,,44.1,33.0 +2019-05-16 01:00:00,,30.9,33.0 +2019-05-16 02:00:00,,27.4,33.0 +2019-05-16 03:00:00,28.0,26.0,28.0 +2019-05-16 04:00:00,,26.7,28.0 +2019-05-16 05:00:00,,27.9,26.0 +2019-05-16 06:00:00,,37.0,26.0 +2019-05-16 07:00:00,,52.6,33.0 +2019-05-16 08:00:00,,,34.0 +2019-05-16 09:00:00,,40.0,33.0 +2019-05-16 10:00:00,,39.4,32.0 +2019-05-16 11:00:00,,29.5,31.0 +2019-05-16 12:00:00,,13.5,33.0 +2019-05-16 13:00:00,,10.5,30.0 +2019-05-16 14:00:00,,9.2,27.0 +2019-05-16 15:00:00,,8.5,27.0 +2019-05-16 16:00:00,,8.1,26.0 +2019-05-16 17:00:00,,10.1,29.0 +2019-05-16 18:00:00,,10.3,30.0 +2019-05-16 19:00:00,,13.5,25.0 +2019-05-16 20:00:00,,15.9,27.0 +2019-05-16 21:00:00,,14.4,26.0 +2019-05-16 22:00:00,,24.8,25.0 +2019-05-16 23:00:00,,24.3,25.0 +2019-05-17 00:00:00,,37.1,25.0 +2019-05-17 01:00:00,,43.7,23.0 +2019-05-17 02:00:00,,46.3,23.0 +2019-05-17 03:00:00,,26.1,21.0 +2019-05-17 04:00:00,,24.6,21.0 +2019-05-17 05:00:00,,26.6,21.0 +2019-05-17 06:00:00,,28.4,21.0 +2019-05-17 07:00:00,,34.0,25.0 +2019-05-17 08:00:00,,46.3,27.0 +2019-05-17 09:00:00,,55.0,27.0 +2019-05-17 10:00:00,,57.5,29.0 +2019-05-17 11:00:00,,60.5,30.0 +2019-05-17 12:00:00,,51.5,30.0 +2019-05-17 13:00:00,,43.1,30.0 +2019-05-17 14:00:00,,46.5,29.0 +2019-05-17 15:00:00,,37.9,31.0 +2019-05-17 16:00:00,,27.0,32.0 +2019-05-17 17:00:00,,22.2,30.0 +2019-05-17 18:00:00,,20.7,29.0 +2019-05-17 19:00:00,,27.9,31.0 +2019-05-17 20:00:00,,33.6,36.0 +2019-05-17 21:00:00,,24.7,36.0 +2019-05-17 22:00:00,,23.5,36.0 +2019-05-17 23:00:00,,24.3,35.0 +2019-05-18 00:00:00,,28.2,35.0 +2019-05-18 01:00:00,,34.1,31.0 +2019-05-18 02:00:00,,31.5,31.0 +2019-05-18 03:00:00,41.5,37.4,31.0 +2019-05-18 04:00:00,,29.0,31.0 +2019-05-18 05:00:00,,16.1,29.0 +2019-05-18 06:00:00,,16.6,29.0 +2019-05-18 07:00:00,,20.1,27.0 +2019-05-18 08:00:00,,22.1,29.0 +2019-05-18 09:00:00,,27.4,35.0 +2019-05-18 10:00:00,,20.4,32.0 +2019-05-18 11:00:00,,21.1,35.0 +2019-05-18 12:00:00,,24.1,34.0 +2019-05-18 13:00:00,,17.5,38.0 +2019-05-18 14:00:00,,12.9,29.0 +2019-05-18 15:00:00,,10.5,27.0 +2019-05-18 16:00:00,,11.8,28.0 +2019-05-18 17:00:00,,13.0,30.0 +2019-05-18 18:00:00,,14.6,42.0 +2019-05-18 19:00:00,,12.8,42.0 +2019-05-18 20:00:00,35.5,14.5,36.0 +2019-05-18 21:00:00,35.5,67.5,35.0 +2019-05-18 22:00:00,40.0,36.2,41.0 +2019-05-18 23:00:00,39.0,59.3,46.0 +2019-05-19 00:00:00,34.5,62.5,46.0 +2019-05-19 01:00:00,29.5,50.2,49.0 +2019-05-19 02:00:00,23.5,49.6,49.0 +2019-05-19 03:00:00,22.5,34.9,49.0 +2019-05-19 04:00:00,19.0,38.1,49.0 +2019-05-19 05:00:00,19.0,36.4,49.0 +2019-05-19 06:00:00,21.0,39.4,49.0 +2019-05-19 07:00:00,26.0,40.9,38.0 +2019-05-19 08:00:00,30.5,31.1,36.0 +2019-05-19 09:00:00,30.0,32.4,33.0 +2019-05-19 10:00:00,23.5,31.7,30.0 +2019-05-19 11:00:00,16.0,33.0,27.0 +2019-05-19 12:00:00,17.5,31.0,28.0 +2019-05-19 13:00:00,17.0,32.6,25.0 +2019-05-19 14:00:00,16.0,27.9,27.0 +2019-05-19 15:00:00,14.5,21.0,31.0 +2019-05-19 16:00:00,23.0,23.8,29.0 +2019-05-19 17:00:00,33.0,31.7,28.0 +2019-05-19 18:00:00,17.5,32.5,27.0 +2019-05-19 19:00:00,18.5,33.9,29.0 +2019-05-19 20:00:00,15.5,32.7,30.0 +2019-05-19 21:00:00,26.0,51.2,32.0 +2019-05-19 22:00:00,15.0,35.6,32.0 +2019-05-19 23:00:00,12.5,23.2,32.0 +2019-05-20 00:00:00,18.5,22.2,32.0 +2019-05-20 01:00:00,16.5,18.8,28.0 +2019-05-20 02:00:00,26.0,16.4,28.0 +2019-05-20 03:00:00,17.0,12.8,32.0 +2019-05-20 04:00:00,10.5,12.1,32.0 +2019-05-20 05:00:00,9.0,12.6,26.0 +2019-05-20 06:00:00,14.0,14.9,26.0 +2019-05-20 07:00:00,20.0,25.2,31.0 +2019-05-20 08:00:00,26.0,40.1,31.0 +2019-05-20 09:00:00,38.0,46.9,29.0 +2019-05-20 10:00:00,40.0,46.1,29.0 +2019-05-20 11:00:00,30.5,45.5,28.0 +2019-05-20 12:00:00,25.0,43.9,28.0 +2019-05-20 13:00:00,25.0,35.4,28.0 +2019-05-20 14:00:00,34.5,23.8,29.0 +2019-05-20 15:00:00,32.0,23.7,32.0 +2019-05-20 16:00:00,24.5,27.5,32.0 +2019-05-20 17:00:00,25.5,26.5,29.0 +2019-05-20 18:00:00,,32.4,30.0 +2019-05-20 19:00:00,,24.6,33.0 +2019-05-20 20:00:00,,32.2,32.0 +2019-05-20 21:00:00,,21.3,32.0 +2019-05-20 22:00:00,,21.6,34.0 +2019-05-20 23:00:00,,20.3,47.0 +2019-05-21 00:00:00,,20.7,47.0 +2019-05-21 01:00:00,,19.6,35.0 +2019-05-21 02:00:00,,16.9,35.0 +2019-05-21 03:00:00,15.5,16.3,26.0 +2019-05-21 04:00:00,,17.7,26.0 +2019-05-21 05:00:00,,17.9,23.0 +2019-05-21 06:00:00,,18.5,23.0 +2019-05-21 07:00:00,,38.0,30.0 +2019-05-21 08:00:00,,62.6,27.0 +2019-05-21 09:00:00,,56.0,28.0 +2019-05-21 10:00:00,,54.2,29.0 +2019-05-21 11:00:00,,48.1,29.0 +2019-05-21 12:00:00,,30.4,26.0 +2019-05-21 13:00:00,,25.5,26.0 +2019-05-21 14:00:00,,30.5,28.0 +2019-05-21 15:00:00,,49.7,33.0 +2019-05-21 16:00:00,,47.8,34.0 +2019-05-21 17:00:00,,36.6,34.0 +2019-05-21 18:00:00,,42.3,37.0 +2019-05-21 19:00:00,,75.0,35.0 +2019-05-21 20:00:00,,54.3,40.0 +2019-05-21 21:00:00,,50.0,38.0 +2019-05-21 22:00:00,,40.8,33.0 +2019-05-21 23:00:00,,43.0,33.0 +2019-05-22 00:00:00,,33.2,33.0 +2019-05-22 01:00:00,,29.5,30.0 +2019-05-22 02:00:00,,27.1,30.0 +2019-05-22 03:00:00,20.5,27.9,27.0 +2019-05-22 04:00:00,,19.2,27.0 +2019-05-22 05:00:00,,25.2,21.0 +2019-05-22 06:00:00,,33.7,21.0 +2019-05-22 07:00:00,,45.1,28.0 +2019-05-22 08:00:00,,75.7,29.0 +2019-05-22 09:00:00,,75.4,31.0 +2019-05-22 10:00:00,,70.8,31.0 +2019-05-22 11:00:00,,63.1,31.0 +2019-05-22 12:00:00,,57.8,28.0 +2019-05-22 13:00:00,,42.6,25.0 +2019-05-22 14:00:00,,42.2,25.0 +2019-05-22 15:00:00,,38.5,28.0 +2019-05-22 16:00:00,,40.0,30.0 +2019-05-22 17:00:00,,33.2,32.0 +2019-05-22 18:00:00,,34.9,34.0 +2019-05-22 19:00:00,,36.1,34.0 +2019-05-22 20:00:00,,34.1,33.0 +2019-05-22 21:00:00,,36.2,33.0 +2019-05-22 22:00:00,,44.9,31.0 +2019-05-22 23:00:00,,37.7,32.0 +2019-05-23 00:00:00,,29.8,32.0 +2019-05-23 01:00:00,,62.1,23.0 +2019-05-23 02:00:00,,53.3,23.0 +2019-05-23 03:00:00,60.5,53.1,20.0 +2019-05-23 04:00:00,,66.6,20.0 +2019-05-23 05:00:00,,76.8,19.0 +2019-05-23 06:00:00,,71.9,19.0 +2019-05-23 07:00:00,,68.7,24.0 +2019-05-23 08:00:00,,79.6,26.0 +2019-05-23 09:00:00,,91.8,25.0 +2019-05-23 10:00:00,,97.0,23.0 +2019-05-23 11:00:00,,79.4,25.0 +2019-05-23 12:00:00,,28.3,24.0 +2019-05-23 13:00:00,,17.0,25.0 +2019-05-23 14:00:00,,16.4,28.0 +2019-05-23 15:00:00,,21.2,34.0 +2019-05-23 16:00:00,,17.2,38.0 +2019-05-23 17:00:00,,17.5,53.0 +2019-05-23 18:00:00,,17.8,60.0 +2019-05-23 19:00:00,,22.7,54.0 +2019-05-23 20:00:00,,23.5,51.0 +2019-05-23 21:00:00,,28.0,45.0 +2019-05-23 22:00:00,,33.8,44.0 +2019-05-23 23:00:00,,47.0,39.0 +2019-05-24 00:00:00,,61.9,39.0 +2019-05-24 01:00:00,,23.2,31.0 +2019-05-24 02:00:00,,32.8, +2019-05-24 03:00:00,74.5,28.8,31.0 +2019-05-24 04:00:00,,28.4,31.0 +2019-05-24 05:00:00,,19.4,23.0 +2019-05-24 06:00:00,,28.1,23.0 +2019-05-24 07:00:00,,35.9,29.0 +2019-05-24 08:00:00,,40.7,28.0 +2019-05-24 09:00:00,,54.8,26.0 +2019-05-24 10:00:00,,45.9,24.0 +2019-05-24 11:00:00,,37.9,23.0 +2019-05-24 12:00:00,,28.6,26.0 +2019-05-24 13:00:00,,40.6,29.0 +2019-05-24 14:00:00,,29.3,33.0 +2019-05-24 15:00:00,,24.3,39.0 +2019-05-24 16:00:00,,20.5,40.0 +2019-05-24 17:00:00,,22.7,43.0 +2019-05-24 18:00:00,,27.3,46.0 +2019-05-24 19:00:00,,25.2,46.0 +2019-05-24 20:00:00,,23.3,44.0 +2019-05-24 21:00:00,,21.9,42.0 +2019-05-24 22:00:00,,31.7,38.0 +2019-05-24 23:00:00,,18.1,39.0 +2019-05-25 00:00:00,,18.0,39.0 +2019-05-25 01:00:00,,16.5,32.0 +2019-05-25 02:00:00,,17.4,32.0 +2019-05-25 03:00:00,29.0,12.8,25.0 +2019-05-25 04:00:00,,20.3,25.0 +2019-05-25 05:00:00,,,21.0 +2019-05-25 06:00:00,,,21.0 +2019-05-25 07:00:00,,,22.0 +2019-05-25 08:00:00,,36.9,22.0 +2019-05-25 09:00:00,,42.1,23.0 +2019-05-25 10:00:00,,44.5,23.0 +2019-05-25 11:00:00,,33.6,21.0 +2019-05-25 12:00:00,,26.3,23.0 +2019-05-25 13:00:00,,19.5,24.0 +2019-05-25 14:00:00,,18.6,26.0 +2019-05-25 15:00:00,,26.1,31.0 +2019-05-25 16:00:00,,23.6,37.0 +2019-05-25 17:00:00,,30.0,42.0 +2019-05-25 18:00:00,,31.9,46.0 +2019-05-25 19:00:00,,20.6,47.0 +2019-05-25 20:00:00,,30.4,47.0 +2019-05-25 21:00:00,,22.1,44.0 +2019-05-25 22:00:00,,43.6,41.0 +2019-05-25 23:00:00,,39.5,36.0 +2019-05-26 00:00:00,,63.9,36.0 +2019-05-26 01:00:00,,70.2,32.0 +2019-05-26 02:00:00,,67.0,32.0 +2019-05-26 03:00:00,53.0,49.8,26.0 +2019-05-26 04:00:00,,23.4,26.0 +2019-05-26 05:00:00,,22.9,20.0 +2019-05-26 06:00:00,,22.3,20.0 +2019-05-26 07:00:00,,16.8,17.0 +2019-05-26 08:00:00,,15.1,17.0 +2019-05-26 09:00:00,,13.4,15.0 +2019-05-26 10:00:00,,11.0,15.0 +2019-05-26 11:00:00,,10.3,16.0 +2019-05-26 12:00:00,,11.3,17.0 +2019-05-26 13:00:00,,13.3,21.0 +2019-05-26 14:00:00,,11.5,24.0 +2019-05-26 15:00:00,,12.5,25.0 +2019-05-26 16:00:00,,15.3,26.0 +2019-05-26 17:00:00,,11.7,27.0 +2019-05-26 18:00:00,,17.1,26.0 +2019-05-26 19:00:00,,17.3,28.0 +2019-05-26 20:00:00,,22.8,26.0 +2019-05-26 21:00:00,,17.8,25.0 +2019-05-26 22:00:00,,16.6,27.0 +2019-05-26 23:00:00,,16.1,26.0 +2019-05-27 00:00:00,,15.2,26.0 +2019-05-27 01:00:00,,10.3,26.0 +2019-05-27 02:00:00,,9.5,26.0 +2019-05-27 03:00:00,10.5,7.1,24.0 +2019-05-27 04:00:00,,5.9,24.0 +2019-05-27 05:00:00,,4.8,19.0 +2019-05-27 06:00:00,,6.5,19.0 +2019-05-27 07:00:00,,20.3,18.0 +2019-05-27 08:00:00,,29.1,18.0 +2019-05-27 09:00:00,,29.5,18.0 +2019-05-27 10:00:00,,34.2,18.0 +2019-05-27 11:00:00,,31.4,16.0 +2019-05-27 12:00:00,,23.3,17.0 +2019-05-27 13:00:00,,19.3,17.0 +2019-05-27 14:00:00,,17.3,20.0 +2019-05-27 15:00:00,,17.5,20.0 +2019-05-27 16:00:00,,17.3,22.0 +2019-05-27 17:00:00,,25.6,22.0 +2019-05-27 18:00:00,,23.6,22.0 +2019-05-27 19:00:00,,22.9,22.0 +2019-05-27 20:00:00,,25.6,22.0 +2019-05-27 21:00:00,,22.1,23.0 +2019-05-27 22:00:00,,22.3,20.0 +2019-05-27 23:00:00,,18.8,19.0 +2019-05-28 00:00:00,,19.9,19.0 +2019-05-28 01:00:00,,22.6,16.0 +2019-05-28 02:00:00,,15.4,16.0 +2019-05-28 03:00:00,11.0,8.2,16.0 +2019-05-28 04:00:00,,6.4,16.0 +2019-05-28 05:00:00,,6.1,15.0 +2019-05-28 06:00:00,,8.9,15.0 +2019-05-28 07:00:00,,19.9,19.0 +2019-05-28 08:00:00,,28.8,20.0 +2019-05-28 09:00:00,,33.8,20.0 +2019-05-28 10:00:00,,31.2,20.0 +2019-05-28 11:00:00,,24.3,21.0 +2019-05-28 12:00:00,,21.6,21.0 +2019-05-28 13:00:00,,20.5,28.0 +2019-05-28 14:00:00,,24.8,27.0 +2019-05-28 15:00:00,,18.5,29.0 +2019-05-28 16:00:00,,18.8,30.0 +2019-05-28 17:00:00,,25.0,27.0 +2019-05-28 18:00:00,,26.5,25.0 +2019-05-28 19:00:00,,20.8,29.0 +2019-05-28 20:00:00,,16.2,29.0 +2019-05-28 21:00:00,,18.5,29.0 +2019-05-28 22:00:00,,20.4,31.0 +2019-05-28 23:00:00,,20.4, +2019-05-29 00:00:00,,20.2,25.0 +2019-05-29 01:00:00,,25.3,26.0 +2019-05-29 02:00:00,,23.4,26.0 +2019-05-29 03:00:00,21.0,21.6,23.0 +2019-05-29 04:00:00,,19.0,23.0 +2019-05-29 05:00:00,,20.3,21.0 +2019-05-29 06:00:00,,24.1,21.0 +2019-05-29 07:00:00,,36.7,24.0 +2019-05-29 08:00:00,,46.5,22.0 +2019-05-29 09:00:00,,50.5,21.0 +2019-05-29 10:00:00,,45.7,18.0 +2019-05-29 11:00:00,,34.5,18.0 +2019-05-29 12:00:00,,30.7,18.0 +2019-05-29 13:00:00,,22.0,20.0 +2019-05-29 14:00:00,,13.2,13.0 +2019-05-29 15:00:00,,17.8,15.0 +2019-05-29 16:00:00,,0.0,5.0 +2019-05-29 17:00:00,,0.0,3.0 +2019-05-29 18:00:00,,20.1,5.0 +2019-05-29 19:00:00,,22.9,5.0 +2019-05-29 20:00:00,,25.3,5.0 +2019-05-29 21:00:00,,24.1,6.0 +2019-05-29 22:00:00,,20.8,6.0 +2019-05-29 23:00:00,,16.9,5.0 +2019-05-30 00:00:00,,19.0,5.0 +2019-05-30 01:00:00,,19.9,1.0 +2019-05-30 02:00:00,,19.4,1.0 +2019-05-30 03:00:00,7.5,12.4,0.0 +2019-05-30 04:00:00,,9.4,0.0 +2019-05-30 05:00:00,,10.6,0.0 +2019-05-30 06:00:00,,10.4,0.0 +2019-05-30 07:00:00,,12.2,0.0 +2019-05-30 08:00:00,,13.3,2.0 +2019-05-30 09:00:00,,18.3,3.0 +2019-05-30 10:00:00,,16.7,5.0 +2019-05-30 11:00:00,,15.1,9.0 +2019-05-30 12:00:00,,13.8,13.0 +2019-05-30 13:00:00,,14.9,17.0 +2019-05-30 14:00:00,,14.2,20.0 +2019-05-30 15:00:00,,16.1,22.0 +2019-05-30 16:00:00,,14.9,22.0 +2019-05-30 17:00:00,,13.0,27.0 +2019-05-30 18:00:00,,12.8,30.0 +2019-05-30 19:00:00,,20.4,28.0 +2019-05-30 20:00:00,,22.1,28.0 +2019-05-30 21:00:00,,22.9,27.0 +2019-05-30 22:00:00,,21.9,27.0 +2019-05-30 23:00:00,,26.9,23.0 +2019-05-31 00:00:00,,27.0,23.0 +2019-05-31 01:00:00,,29.6,18.0 +2019-05-31 02:00:00,,27.2,18.0 +2019-05-31 03:00:00,9.0,36.9,12.0 +2019-05-31 04:00:00,,44.1,12.0 +2019-05-31 05:00:00,,40.1,9.0 +2019-05-31 06:00:00,,31.1,9.0 +2019-05-31 07:00:00,,37.2,8.0 +2019-05-31 08:00:00,,38.6,9.0 +2019-05-31 09:00:00,,47.4,8.0 +2019-05-31 10:00:00,,36.6,37.0 +2019-05-31 11:00:00,,19.6,15.0 +2019-05-31 12:00:00,,17.2,16.0 +2019-05-31 13:00:00,,15.1,18.0 +2019-05-31 14:00:00,,13.3,21.0 +2019-05-31 15:00:00,,13.8,21.0 +2019-05-31 16:00:00,,15.4,24.0 +2019-05-31 17:00:00,,15.4,26.0 +2019-05-31 18:00:00,,16.3,26.0 +2019-05-31 19:00:00,,20.5,29.0 +2019-05-31 20:00:00,,25.2,33.0 +2019-05-31 21:00:00,,23.3,33.0 +2019-05-31 22:00:00,,37.0,31.0 +2019-05-31 23:00:00,,60.2,26.0 +2019-06-01 00:00:00,,68.0,26.0 +2019-06-01 01:00:00,,81.7,22.0 +2019-06-01 02:00:00,,84.7,22.0 +2019-06-01 03:00:00,52.5,74.8,16.0 +2019-06-01 04:00:00,,68.1,16.0 +2019-06-01 05:00:00,,,11.0 +2019-06-01 06:00:00,,,11.0 +2019-06-01 07:00:00,,,4.0 +2019-06-01 08:00:00,,44.6,2.0 +2019-06-01 09:00:00,,46.4,8.0 +2019-06-01 10:00:00,,33.3,9.0 +2019-06-01 11:00:00,,23.9,12.0 +2019-06-01 12:00:00,,13.8,19.0 +2019-06-01 13:00:00,,12.2,28.0 +2019-06-01 14:00:00,,10.4,33.0 +2019-06-01 15:00:00,,10.2,36.0 +2019-06-01 16:00:00,,10.0,33.0 +2019-06-01 17:00:00,,10.2,31.0 +2019-06-01 18:00:00,,11.8,32.0 +2019-06-01 19:00:00,,11.8,36.0 +2019-06-01 20:00:00,,14.5,38.0 +2019-06-01 21:00:00,,24.6,41.0 +2019-06-01 22:00:00,,43.6,44.0 +2019-06-01 23:00:00,,49.4,52.0 +2019-06-02 00:00:00,,48.1,52.0 +2019-06-02 01:00:00,,32.7,44.0 +2019-06-02 02:00:00,,38.1,44.0 +2019-06-02 03:00:00,,38.2,43.0 +2019-06-02 04:00:00,,39.2,43.0 +2019-06-02 05:00:00,,23.2,37.0 +2019-06-02 06:00:00,,24.5,37.0 +2019-06-02 07:00:00,,37.2,32.0 +2019-06-02 08:00:00,,24.1,32.0 +2019-06-02 09:00:00,,18.1,30.0 +2019-06-02 10:00:00,,19.5,32.0 +2019-06-02 11:00:00,,21.0,35.0 +2019-06-02 12:00:00,,18.1,36.0 +2019-06-02 13:00:00,,13.1,35.0 +2019-06-02 14:00:00,,11.5,34.0 +2019-06-02 15:00:00,,13.0,36.0 +2019-06-02 16:00:00,,15.0,33.0 +2019-06-02 17:00:00,,13.9,32.0 +2019-06-02 18:00:00,,14.4,32.0 +2019-06-02 19:00:00,,14.4,34.0 +2019-06-02 20:00:00,,15.6,34.0 +2019-06-02 21:00:00,,25.8,32.0 +2019-06-02 22:00:00,,40.9,28.0 +2019-06-02 23:00:00,,36.9,27.0 +2019-06-03 00:00:00,,27.6,27.0 +2019-06-03 01:00:00,,17.9,21.0 +2019-06-03 02:00:00,,15.7,21.0 +2019-06-03 03:00:00,,11.8,11.0 +2019-06-03 04:00:00,,11.7,11.0 +2019-06-03 05:00:00,,9.8,3.0 +2019-06-03 06:00:00,,11.4,3.0 +2019-06-03 07:00:00,,29.0,5.0 +2019-06-03 08:00:00,,44.1,6.0 +2019-06-03 09:00:00,,50.0,7.0 +2019-06-03 10:00:00,,43.9,5.0 +2019-06-03 11:00:00,,46.0,11.0 +2019-06-03 12:00:00,,31.7,16.0 +2019-06-03 13:00:00,,27.5,14.0 +2019-06-03 14:00:00,,22.1,15.0 +2019-06-03 15:00:00,,25.8,17.0 +2019-06-03 16:00:00,,23.2,21.0 +2019-06-03 17:00:00,,24.8,22.0 +2019-06-03 18:00:00,,25.3,24.0 +2019-06-03 19:00:00,,24.4,24.0 +2019-06-03 20:00:00,,23.1,23.0 +2019-06-03 21:00:00,,28.9,20.0 +2019-06-03 22:00:00,,33.0,20.0 +2019-06-03 23:00:00,,31.1,17.0 +2019-06-04 00:00:00,,30.5,17.0 +2019-06-04 01:00:00,,44.6,12.0 +2019-06-04 02:00:00,,52.4,12.0 +2019-06-04 03:00:00,,43.9,8.0 +2019-06-04 04:00:00,,35.0,8.0 +2019-06-04 05:00:00,,41.6,5.0 +2019-06-04 06:00:00,,28.8,5.0 +2019-06-04 07:00:00,,36.5,14.0 +2019-06-04 08:00:00,,47.7,18.0 +2019-06-04 09:00:00,,53.5,22.0 +2019-06-04 10:00:00,,50.8,35.0 +2019-06-04 11:00:00,,38.5,31.0 +2019-06-04 12:00:00,,23.3,32.0 +2019-06-04 13:00:00,,19.6,35.0 +2019-06-04 14:00:00,,17.7,37.0 +2019-06-04 15:00:00,,17.4,36.0 +2019-06-04 16:00:00,,18.1,38.0 +2019-06-04 17:00:00,,21.5,38.0 +2019-06-04 18:00:00,,26.3,40.0 +2019-06-04 19:00:00,,23.4,29.0 +2019-06-04 20:00:00,,25.2,20.0 +2019-06-04 21:00:00,,17.0,18.0 +2019-06-04 22:00:00,,16.9,17.0 +2019-06-04 23:00:00,,26.3,17.0 +2019-06-05 00:00:00,,33.5,17.0 +2019-06-05 01:00:00,,17.8,13.0 +2019-06-05 02:00:00,,15.7,13.0 +2019-06-05 03:00:00,15.0,10.8,4.0 +2019-06-05 04:00:00,,12.4,4.0 +2019-06-05 05:00:00,,16.2,6.0 +2019-06-05 06:00:00,,24.5,6.0 +2019-06-05 07:00:00,,39.2,2.0 +2019-06-05 08:00:00,,35.8,1.0 +2019-06-05 09:00:00,,36.9,0.0 +2019-06-05 10:00:00,,35.3,0.0 +2019-06-05 11:00:00,,36.8,5.0 +2019-06-05 12:00:00,,42.1,7.0 +2019-06-05 13:00:00,,59.0,9.0 +2019-06-05 14:00:00,,47.2,14.0 +2019-06-05 15:00:00,,33.6,20.0 +2019-06-05 16:00:00,,38.3,20.0 +2019-06-05 17:00:00,,53.5,19.0 +2019-06-05 18:00:00,,37.9,19.0 +2019-06-05 19:00:00,,48.8,19.0 +2019-06-05 20:00:00,,40.8,19.0 +2019-06-05 21:00:00,,37.8,19.0 +2019-06-05 22:00:00,,37.5,19.0 +2019-06-05 23:00:00,,33.7,17.0 +2019-06-06 00:00:00,,30.3,17.0 +2019-06-06 01:00:00,,31.8,8.0 +2019-06-06 02:00:00,,23.8, +2019-06-06 03:00:00,,18.0,4.0 +2019-06-06 04:00:00,,15.2,4.0 +2019-06-06 05:00:00,,19.2,0.0 +2019-06-06 06:00:00,,28.4,0.0 +2019-06-06 07:00:00,,40.3,1.0 +2019-06-06 08:00:00,,40.5,3.0 +2019-06-06 09:00:00,,43.1,0.0 +2019-06-06 10:00:00,,36.0,1.0 +2019-06-06 11:00:00,,26.0,7.0 +2019-06-06 12:00:00,,21.2,7.0 +2019-06-06 13:00:00,,16.4,12.0 +2019-06-06 14:00:00,,16.5,10.0 +2019-06-06 15:00:00,,16.0,11.0 +2019-06-06 16:00:00,,15.1,16.0 +2019-06-06 17:00:00,,,22.0 +2019-06-06 18:00:00,,,24.0 +2019-06-06 19:00:00,,,24.0 +2019-06-06 20:00:00,,,24.0 +2019-06-06 21:00:00,,,22.0 +2019-06-06 22:00:00,,,24.0 +2019-06-06 23:00:00,,,21.0 +2019-06-07 00:00:00,,,21.0 +2019-06-07 01:00:00,,,23.0 +2019-06-07 02:00:00,,,23.0 +2019-06-07 03:00:00,,,27.0 +2019-06-07 04:00:00,,,27.0 +2019-06-07 05:00:00,,,23.0 +2019-06-07 06:00:00,,,23.0 +2019-06-07 07:00:00,,,25.0 +2019-06-07 08:00:00,,28.9,23.0 +2019-06-07 09:00:00,,23.0,24.0 +2019-06-07 10:00:00,,29.3,25.0 +2019-06-07 11:00:00,,34.5,23.0 +2019-06-07 12:00:00,,32.1,25.0 +2019-06-07 13:00:00,,26.7,27.0 +2019-06-07 14:00:00,,17.8,20.0 +2019-06-07 15:00:00,,15.0,15.0 +2019-06-07 16:00:00,,13.1,15.0 +2019-06-07 17:00:00,,15.6,21.0 +2019-06-07 18:00:00,,19.5,24.0 +2019-06-07 19:00:00,,19.5,27.0 +2019-06-07 20:00:00,,19.1,35.0 +2019-06-07 21:00:00,,19.9,36.0 +2019-06-07 22:00:00,,19.4,35.0 +2019-06-07 23:00:00,,16.3, +2019-06-08 00:00:00,,14.7,33.0 +2019-06-08 01:00:00,,14.4,28.0 +2019-06-08 02:00:00,,11.3, +2019-06-08 03:00:00,,9.6,7.0 +2019-06-08 04:00:00,,8.4,7.0 +2019-06-08 05:00:00,,9.8,3.0 +2019-06-08 06:00:00,,10.7,3.0 +2019-06-08 07:00:00,,14.1,2.0 +2019-06-08 08:00:00,,13.8,3.0 +2019-06-08 09:00:00,,14.0,4.0 +2019-06-08 10:00:00,,13.0,2.0 +2019-06-08 11:00:00,,11.7,3.0 +2019-06-08 12:00:00,,10.3,4.0 +2019-06-08 13:00:00,,10.4,8.0 +2019-06-08 14:00:00,,9.2,10.0 +2019-06-08 15:00:00,,11.1,13.0 +2019-06-08 16:00:00,,10.3,17.0 +2019-06-08 17:00:00,,11.7,19.0 +2019-06-08 18:00:00,,14.1,20.0 +2019-06-08 19:00:00,,14.8,20.0 +2019-06-08 20:00:00,,22.0,19.0 +2019-06-08 21:00:00,,,17.0 +2019-06-08 22:00:00,,,16.0 +2019-06-08 23:00:00,,36.7, +2019-06-09 00:00:00,,34.8,20.0 +2019-06-09 01:00:00,,47.0,10.0 +2019-06-09 02:00:00,,55.9,10.0 +2019-06-09 03:00:00,10.0,41.0,7.0 +2019-06-09 04:00:00,,51.2,7.0 +2019-06-09 05:00:00,,51.5,1.0 +2019-06-09 06:00:00,,43.0,1.0 +2019-06-09 07:00:00,,42.2,5.0 +2019-06-09 08:00:00,,36.7,1.0 +2019-06-09 09:00:00,,32.7,0.0 +2019-06-09 10:00:00,,30.2,0.0 +2019-06-09 11:00:00,,25.0,2.0 +2019-06-09 12:00:00,,16.6,5.0 +2019-06-09 13:00:00,,14.6,8.0 +2019-06-09 14:00:00,,14.6,13.0 +2019-06-09 15:00:00,,10.2,17.0 +2019-06-09 16:00:00,,7.9,19.0 +2019-06-09 17:00:00,,7.2,24.0 +2019-06-09 18:00:00,,10.3,26.0 +2019-06-09 19:00:00,,13.0,20.0 +2019-06-09 20:00:00,,19.5,21.0 +2019-06-09 21:00:00,,30.6,21.0 +2019-06-09 22:00:00,,33.2,22.0 +2019-06-09 23:00:00,,30.9, +2019-06-10 00:00:00,,37.1,24.0 +2019-06-10 01:00:00,,39.9,21.0 +2019-06-10 02:00:00,,28.1,21.0 +2019-06-10 03:00:00,18.5,19.3,25.0 +2019-06-10 04:00:00,,17.8,25.0 +2019-06-10 05:00:00,,18.0,24.0 +2019-06-10 06:00:00,,13.7,24.0 +2019-06-10 07:00:00,,21.3,24.0 +2019-06-10 08:00:00,,26.7,22.0 +2019-06-10 09:00:00,,23.0,27.0 +2019-06-10 10:00:00,,16.9,34.0 +2019-06-10 11:00:00,,18.5,45.0 +2019-06-10 12:00:00,,14.1,41.0 +2019-06-10 13:00:00,,12.2,45.0 +2019-06-10 14:00:00,,11.7,51.0 +2019-06-10 15:00:00,,9.6,40.0 +2019-06-10 16:00:00,,9.5,40.0 +2019-06-10 17:00:00,,11.7,31.0 +2019-06-10 18:00:00,,15.1,28.0 +2019-06-10 19:00:00,,19.1,26.0 +2019-06-10 20:00:00,,18.4,25.0 +2019-06-10 21:00:00,,22.3,26.0 +2019-06-10 22:00:00,,22.6,24.0 +2019-06-10 23:00:00,,23.5,23.0 +2019-06-11 00:00:00,,24.8,23.0 +2019-06-11 01:00:00,,24.1,15.0 +2019-06-11 02:00:00,,19.6,15.0 +2019-06-11 03:00:00,7.5,19.1,16.0 +2019-06-11 04:00:00,,29.6,16.0 +2019-06-11 05:00:00,,32.3,13.0 +2019-06-11 06:00:00,,52.7,13.0 +2019-06-11 07:00:00,,58.7,17.0 +2019-06-11 08:00:00,,55.4,18.0 +2019-06-11 09:00:00,,58.0,21.0 +2019-06-11 10:00:00,,43.6,23.0 +2019-06-11 11:00:00,,31.7,22.0 +2019-06-11 12:00:00,,22.1,22.0 +2019-06-11 13:00:00,,17.3,23.0 +2019-06-11 14:00:00,,12.6,26.0 +2019-06-11 15:00:00,,13.1,35.0 +2019-06-11 16:00:00,,16.6,31.0 +2019-06-11 17:00:00,,19.8,31.0 +2019-06-11 18:00:00,,22.6,30.0 +2019-06-11 19:00:00,,35.5,31.0 +2019-06-11 20:00:00,,44.6,30.0 +2019-06-11 21:00:00,,36.1,22.0 +2019-06-11 22:00:00,,42.7,22.0 +2019-06-11 23:00:00,,54.1,20.0 +2019-06-12 00:00:00,,59.4,20.0 +2019-06-12 01:00:00,,41.5,15.0 +2019-06-12 02:00:00,,37.2, +2019-06-12 03:00:00,21.0,41.9, +2019-06-12 04:00:00,,34.7,11.0 +2019-06-12 05:00:00,,36.3,9.0 +2019-06-12 06:00:00,,44.9,9.0 +2019-06-12 07:00:00,,42.7,12.0 +2019-06-12 08:00:00,,38.4,17.0 +2019-06-12 09:00:00,,44.4,20.0 +2019-06-12 10:00:00,,35.5,22.0 +2019-06-12 11:00:00,,26.7,25.0 +2019-06-12 12:00:00,,0.0,35.0 +2019-06-12 13:00:00,,0.0,33.0 +2019-06-12 14:00:00,,15.4,33.0 +2019-06-12 15:00:00,,17.9,35.0 +2019-06-12 16:00:00,,20.3,42.0 +2019-06-12 17:00:00,,16.8,45.0 +2019-06-12 18:00:00,,23.6,43.0 +2019-06-12 19:00:00,,24.2,45.0 +2019-06-12 20:00:00,,25.3,33.0 +2019-06-12 21:00:00,,23.4,41.0 +2019-06-12 22:00:00,,29.2,43.0 +2019-06-12 23:00:00,,29.3, +2019-06-13 00:00:00,,25.6,35.0 +2019-06-13 01:00:00,,26.9,29.0 +2019-06-13 02:00:00,,20.0, +2019-06-13 03:00:00,28.5,18.7,26.0 +2019-06-13 04:00:00,,18.0,26.0 +2019-06-13 05:00:00,,18.8,16.0 +2019-06-13 06:00:00,,24.6,16.0 +2019-06-13 07:00:00,,37.0,19.0 +2019-06-13 08:00:00,,39.8,21.0 +2019-06-13 09:00:00,,40.9,19.0 +2019-06-13 10:00:00,,35.3,16.0 +2019-06-13 11:00:00,,30.2,18.0 +2019-06-13 12:00:00,,24.5,19.0 +2019-06-13 13:00:00,,22.7,19.0 +2019-06-13 14:00:00,,17.9,16.0 +2019-06-13 15:00:00,,18.2,15.0 +2019-06-13 16:00:00,,19.4,13.0 +2019-06-13 17:00:00,,28.8,11.0 +2019-06-13 18:00:00,,36.1,15.0 +2019-06-13 19:00:00,,38.2,14.0 +2019-06-13 20:00:00,,24.0,13.0 +2019-06-13 21:00:00,,27.5,14.0 +2019-06-13 22:00:00,,31.5,15.0 +2019-06-13 23:00:00,,58.8,15.0 +2019-06-14 00:00:00,,77.9,15.0 +2019-06-14 01:00:00,,78.3,13.0 +2019-06-14 02:00:00,,74.2, +2019-06-14 03:00:00,,68.1,8.0 +2019-06-14 04:00:00,,66.6,8.0 +2019-06-14 05:00:00,,48.5,6.0 +2019-06-14 06:00:00,,37.9,6.0 +2019-06-14 07:00:00,,49.3,13.0 +2019-06-14 08:00:00,,64.3,11.0 +2019-06-14 09:00:00,,51.5,11.0 +2019-06-14 10:00:00,,34.3,14.0 +2019-06-14 11:00:00,36.5,27.9,13.0 +2019-06-14 12:00:00,,25.1,13.0 +2019-06-14 13:00:00,,21.8,15.0 +2019-06-14 14:00:00,,17.1,16.0 +2019-06-14 15:00:00,,15.4,22.0 +2019-06-14 16:00:00,,14.2,25.0 +2019-06-14 17:00:00,,15.2,25.0 +2019-06-14 18:00:00,,18.9,26.0 +2019-06-14 19:00:00,,16.6,27.0 +2019-06-14 20:00:00,,19.0,26.0 +2019-06-14 21:00:00,,25.0,26.0 +2019-06-14 22:00:00,,41.9,25.0 +2019-06-14 23:00:00,,55.0,26.0 +2019-06-15 00:00:00,,35.3,26.0 +2019-06-15 01:00:00,,32.1,26.0 +2019-06-15 02:00:00,,29.6, +2019-06-15 03:00:00,17.5,29.0, +2019-06-15 04:00:00,,33.9, +2019-06-15 05:00:00,,,10.0 +2019-06-15 06:00:00,,,10.0 +2019-06-15 07:00:00,,,13.0 +2019-06-15 08:00:00,,35.8,13.0 +2019-06-15 09:00:00,,24.1,8.0 +2019-06-15 10:00:00,,17.6,8.0 +2019-06-15 11:00:00,,14.0,12.0 +2019-06-15 12:00:00,,12.1,14.0 +2019-06-15 13:00:00,,11.1,13.0 +2019-06-15 14:00:00,,9.4,18.0 +2019-06-15 15:00:00,,9.0,17.0 +2019-06-15 16:00:00,,9.6,18.0 +2019-06-15 17:00:00,,10.5,18.0 +2019-06-15 18:00:00,,10.7,20.0 +2019-06-15 19:00:00,,11.1,22.0 +2019-06-15 20:00:00,,14.0,22.0 +2019-06-15 21:00:00,,14.2,21.0 +2019-06-15 22:00:00,,15.2,20.0 +2019-06-15 23:00:00,,17.2,19.0 +2019-06-16 00:00:00,,20.1,19.0 +2019-06-16 01:00:00,,22.6,15.0 +2019-06-16 02:00:00,,16.5,15.0 +2019-06-16 03:00:00,42.5,12.8,12.0 +2019-06-16 04:00:00,,11.4,12.0 +2019-06-16 05:00:00,,11.2,10.0 +2019-06-16 06:00:00,,11.7,10.0 +2019-06-16 07:00:00,,14.0,8.0 +2019-06-16 08:00:00,,11.6,5.0 +2019-06-16 09:00:00,,10.2,4.0 +2019-06-16 10:00:00,,9.9,5.0 +2019-06-16 11:00:00,,9.4,6.0 +2019-06-16 12:00:00,,8.7,6.0 +2019-06-16 13:00:00,,12.9,10.0 +2019-06-16 14:00:00,,11.2,16.0 +2019-06-16 15:00:00,,8.7,23.0 +2019-06-16 16:00:00,,8.1,26.0 +2019-06-16 17:00:00,,8.4,29.0 +2019-06-16 18:00:00,,9.2,29.0 +2019-06-16 19:00:00,,11.8,28.0 +2019-06-16 20:00:00,,12.3,28.0 +2019-06-16 21:00:00,,14.4,27.0 +2019-06-16 22:00:00,,23.3,25.0 +2019-06-16 23:00:00,,42.7, +2019-06-17 00:00:00,,56.6,23.0 +2019-06-17 01:00:00,,67.3,17.0 +2019-06-17 02:00:00,,69.3,17.0 +2019-06-17 03:00:00,42.0,58.8,14.0 +2019-06-17 04:00:00,35.5,53.1,14.0 +2019-06-17 05:00:00,36.0,49.1,11.0 +2019-06-17 06:00:00,39.5,45.7,11.0 +2019-06-17 07:00:00,42.5,44.8,12.0 +2019-06-17 08:00:00,43.5,52.3,13.0 +2019-06-17 09:00:00,45.0,54.4,13.0 +2019-06-17 10:00:00,41.0,51.6,11.0 +2019-06-17 11:00:00,,30.4,11.0 +2019-06-17 12:00:00,,16.0,11.0 +2019-06-17 13:00:00,,15.2, +2019-06-17 14:00:00,,10.1, +2019-06-17 15:00:00,,9.6, +2019-06-17 16:00:00,,11.5, +2019-06-17 17:00:00,,13.1, +2019-06-17 18:00:00,,11.9, +2019-06-17 19:00:00,,14.9, +2019-06-17 20:00:00,,15.4, +2019-06-17 21:00:00,,15.2, +2019-06-17 22:00:00,,20.5, +2019-06-17 23:00:00,,38.3, +2019-06-18 00:00:00,,51.0, +2019-06-18 01:00:00,,73.3, +2019-06-18 02:00:00,,66.2, +2019-06-18 03:00:00,,60.1, +2019-06-18 04:00:00,,39.8, +2019-06-18 05:00:00,,45.5, +2019-06-18 06:00:00,,26.5, +2019-06-18 07:00:00,,33.8, +2019-06-18 08:00:00,,51.4, +2019-06-18 09:00:00,,52.6, +2019-06-18 10:00:00,,49.6, +2019-06-18 21:00:00,,15.3, +2019-06-18 22:00:00,,17.0, +2019-06-18 23:00:00,,23.1, +2019-06-19 00:00:00,,39.3, +2019-06-19 11:00:00,,27.3, +2019-06-19 12:00:00,,26.6, +2019-06-20 15:00:00,,19.4, +2019-06-20 16:00:00,,20.1, +2019-06-20 17:00:00,,19.3, +2019-06-20 18:00:00,,19.0, +2019-06-20 19:00:00,,23.2, +2019-06-20 20:00:00,,23.9, +2019-06-20 21:00:00,,25.3, +2019-06-20 22:00:00,,21.4, +2019-06-20 23:00:00,,24.9, +2019-06-21 00:00:00,,26.5, +2019-06-21 01:00:00,,21.8, +2019-06-21 02:00:00,,20.0, diff --git a/doc/data/air_quality_no2_long.csv b/doc/data/air_quality_no2_long.csv new file mode 100644 index 0000000000000..5d959370b7d48 --- /dev/null +++ b/doc/data/air_quality_no2_long.csv @@ -0,0 +1,2069 @@ +city,country,date.utc,location,parameter,value,unit +Paris,FR,2019-06-21 00:00:00+00:00,FR04014,no2,20.0,µg/m³ +Paris,FR,2019-06-20 23:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-06-20 22:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-06-20 21:00:00+00:00,FR04014,no2,24.9,µg/m³ +Paris,FR,2019-06-20 20:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-06-20 19:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-06-20 18:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-06-20 17:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-06-20 16:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-06-20 15:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-06-20 14:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-06-20 13:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-06-19 10:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-06-19 09:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-06-18 22:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-06-18 21:00:00+00:00,FR04014,no2,23.1,µg/m³ +Paris,FR,2019-06-18 20:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-06-18 19:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-06-18 08:00:00+00:00,FR04014,no2,49.6,µg/m³ +Paris,FR,2019-06-18 07:00:00+00:00,FR04014,no2,52.6,µg/m³ +Paris,FR,2019-06-18 06:00:00+00:00,FR04014,no2,51.4,µg/m³ +Paris,FR,2019-06-18 05:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-06-18 04:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-06-18 03:00:00+00:00,FR04014,no2,45.5,µg/m³ +Paris,FR,2019-06-18 02:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-06-18 01:00:00+00:00,FR04014,no2,60.1,µg/m³ +Paris,FR,2019-06-18 00:00:00+00:00,FR04014,no2,66.2,µg/m³ +Paris,FR,2019-06-17 23:00:00+00:00,FR04014,no2,73.3,µg/m³ +Paris,FR,2019-06-17 22:00:00+00:00,FR04014,no2,51.0,µg/m³ +Paris,FR,2019-06-17 21:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-06-17 20:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-06-17 19:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-17 18:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-06-17 17:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-06-17 16:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-06-17 15:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-17 14:00:00+00:00,FR04014,no2,11.5,µg/m³ +Paris,FR,2019-06-17 13:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-17 12:00:00+00:00,FR04014,no2,10.1,µg/m³ +Paris,FR,2019-06-17 11:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-17 10:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-06-17 09:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-06-17 08:00:00+00:00,FR04014,no2,51.6,µg/m³ +Paris,FR,2019-06-17 07:00:00+00:00,FR04014,no2,54.4,µg/m³ +Paris,FR,2019-06-17 06:00:00+00:00,FR04014,no2,52.3,µg/m³ +Paris,FR,2019-06-17 05:00:00+00:00,FR04014,no2,44.8,µg/m³ +Paris,FR,2019-06-17 04:00:00+00:00,FR04014,no2,45.7,µg/m³ +Paris,FR,2019-06-17 03:00:00+00:00,FR04014,no2,49.1,µg/m³ +Paris,FR,2019-06-17 02:00:00+00:00,FR04014,no2,53.1,µg/m³ +Paris,FR,2019-06-17 01:00:00+00:00,FR04014,no2,58.8,µg/m³ +Paris,FR,2019-06-17 00:00:00+00:00,FR04014,no2,69.3,µg/m³ +Paris,FR,2019-06-16 23:00:00+00:00,FR04014,no2,67.3,µg/m³ +Paris,FR,2019-06-16 22:00:00+00:00,FR04014,no2,56.6,µg/m³ +Paris,FR,2019-06-16 21:00:00+00:00,FR04014,no2,42.7,µg/m³ +Paris,FR,2019-06-16 20:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-06-16 19:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-16 18:00:00+00:00,FR04014,no2,12.3,µg/m³ +Paris,FR,2019-06-16 17:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-16 16:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-06-16 15:00:00+00:00,FR04014,no2,8.4,µg/m³ +Paris,FR,2019-06-16 14:00:00+00:00,FR04014,no2,8.1,µg/m³ +Paris,FR,2019-06-16 13:00:00+00:00,FR04014,no2,8.7,µg/m³ +Paris,FR,2019-06-16 12:00:00+00:00,FR04014,no2,11.2,µg/m³ +Paris,FR,2019-06-16 11:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-06-16 10:00:00+00:00,FR04014,no2,8.7,µg/m³ +Paris,FR,2019-06-16 09:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-06-16 08:00:00+00:00,FR04014,no2,9.9,µg/m³ +Paris,FR,2019-06-16 07:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-16 06:00:00+00:00,FR04014,no2,11.6,µg/m³ +Paris,FR,2019-06-16 05:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-16 04:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-16 03:00:00+00:00,FR04014,no2,11.2,µg/m³ +Paris,FR,2019-06-16 02:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-06-16 01:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-06-16 00:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-06-15 23:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-06-15 22:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-06-15 21:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-06-15 20:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-15 19:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-06-15 18:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-15 17:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-06-15 16:00:00+00:00,FR04014,no2,10.7,µg/m³ +Paris,FR,2019-06-15 15:00:00+00:00,FR04014,no2,10.5,µg/m³ +Paris,FR,2019-06-15 14:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-15 13:00:00+00:00,FR04014,no2,9.0,µg/m³ +Paris,FR,2019-06-15 12:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-06-15 11:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-06-15 10:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-06-15 09:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-15 08:00:00+00:00,FR04014,no2,17.6,µg/m³ +Paris,FR,2019-06-15 07:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-06-15 06:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-06-15 02:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-06-15 01:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-06-15 00:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-06-14 23:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-06-14 22:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-06-14 21:00:00+00:00,FR04014,no2,55.0,µg/m³ +Paris,FR,2019-06-14 20:00:00+00:00,FR04014,no2,41.9,µg/m³ +Paris,FR,2019-06-14 19:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-06-14 18:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-06-14 17:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-06-14 16:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-06-14 15:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-14 14:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-06-14 13:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-06-14 12:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-06-14 11:00:00+00:00,FR04014,no2,21.8,µg/m³ +Paris,FR,2019-06-14 10:00:00+00:00,FR04014,no2,25.1,µg/m³ +Paris,FR,2019-06-14 09:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-06-14 08:00:00+00:00,FR04014,no2,34.3,µg/m³ +Paris,FR,2019-06-14 07:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-06-14 06:00:00+00:00,FR04014,no2,64.3,µg/m³ +Paris,FR,2019-06-14 05:00:00+00:00,FR04014,no2,49.3,µg/m³ +Paris,FR,2019-06-14 04:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-06-14 03:00:00+00:00,FR04014,no2,48.5,µg/m³ +Paris,FR,2019-06-14 02:00:00+00:00,FR04014,no2,66.6,µg/m³ +Paris,FR,2019-06-14 01:00:00+00:00,FR04014,no2,68.1,µg/m³ +Paris,FR,2019-06-14 00:00:00+00:00,FR04014,no2,74.2,µg/m³ +Paris,FR,2019-06-13 23:00:00+00:00,FR04014,no2,78.3,µg/m³ +Paris,FR,2019-06-13 22:00:00+00:00,FR04014,no2,77.9,µg/m³ +Paris,FR,2019-06-13 21:00:00+00:00,FR04014,no2,58.8,µg/m³ +Paris,FR,2019-06-13 20:00:00+00:00,FR04014,no2,31.5,µg/m³ +Paris,FR,2019-06-13 19:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-06-13 18:00:00+00:00,FR04014,no2,24.0,µg/m³ +Paris,FR,2019-06-13 17:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-06-13 16:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-06-13 15:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-06-13 14:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-06-13 13:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-06-13 12:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-06-13 11:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-06-13 10:00:00+00:00,FR04014,no2,24.5,µg/m³ +Paris,FR,2019-06-13 09:00:00+00:00,FR04014,no2,30.2,µg/m³ +Paris,FR,2019-06-13 08:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-06-13 07:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-06-13 06:00:00+00:00,FR04014,no2,39.8,µg/m³ +Paris,FR,2019-06-13 05:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-06-13 04:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-06-13 03:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-06-13 02:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-06-13 01:00:00+00:00,FR04014,no2,18.7,µg/m³ +Paris,FR,2019-06-13 00:00:00+00:00,FR04014,no2,20.0,µg/m³ +Paris,FR,2019-06-12 23:00:00+00:00,FR04014,no2,26.9,µg/m³ +Paris,FR,2019-06-12 22:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-06-12 21:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-06-12 20:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-06-12 19:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-06-12 18:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-06-12 17:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-06-12 16:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-06-12 15:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-06-12 14:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-06-12 13:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-06-12 12:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-06-12 11:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-06-12 10:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-06-12 09:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-06-12 08:00:00+00:00,FR04014,no2,35.5,µg/m³ +Paris,FR,2019-06-12 07:00:00+00:00,FR04014,no2,44.4,µg/m³ +Paris,FR,2019-06-12 06:00:00+00:00,FR04014,no2,38.4,µg/m³ +Paris,FR,2019-06-12 05:00:00+00:00,FR04014,no2,42.7,µg/m³ +Paris,FR,2019-06-12 04:00:00+00:00,FR04014,no2,44.9,µg/m³ +Paris,FR,2019-06-12 03:00:00+00:00,FR04014,no2,36.3,µg/m³ +Paris,FR,2019-06-12 02:00:00+00:00,FR04014,no2,34.7,µg/m³ +Paris,FR,2019-06-12 01:00:00+00:00,FR04014,no2,41.9,µg/m³ +Paris,FR,2019-06-12 00:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-06-11 23:00:00+00:00,FR04014,no2,41.5,µg/m³ +Paris,FR,2019-06-11 22:00:00+00:00,FR04014,no2,59.4,µg/m³ +Paris,FR,2019-06-11 21:00:00+00:00,FR04014,no2,54.1,µg/m³ +Paris,FR,2019-06-11 20:00:00+00:00,FR04014,no2,42.7,µg/m³ +Paris,FR,2019-06-11 19:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-06-11 18:00:00+00:00,FR04014,no2,44.6,µg/m³ +Paris,FR,2019-06-11 17:00:00+00:00,FR04014,no2,35.5,µg/m³ +Paris,FR,2019-06-11 16:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-06-11 15:00:00+00:00,FR04014,no2,19.8,µg/m³ +Paris,FR,2019-06-11 14:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-06-11 13:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-11 12:00:00+00:00,FR04014,no2,12.6,µg/m³ +Paris,FR,2019-06-11 11:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-06-11 10:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-06-11 09:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-06-11 08:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-06-11 07:00:00+00:00,FR04014,no2,58.0,µg/m³ +Paris,FR,2019-06-11 06:00:00+00:00,FR04014,no2,55.4,µg/m³ +Paris,FR,2019-06-11 05:00:00+00:00,FR04014,no2,58.7,µg/m³ +Paris,FR,2019-06-11 04:00:00+00:00,FR04014,no2,52.7,µg/m³ +Paris,FR,2019-06-11 03:00:00+00:00,FR04014,no2,32.3,µg/m³ +Paris,FR,2019-06-11 02:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-06-11 01:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-06-11 00:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-06-10 23:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-06-10 22:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-06-10 21:00:00+00:00,FR04014,no2,23.5,µg/m³ +Paris,FR,2019-06-10 20:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-06-10 19:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-06-10 18:00:00+00:00,FR04014,no2,18.4,µg/m³ +Paris,FR,2019-06-10 17:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-06-10 16:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-06-10 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-10 14:00:00+00:00,FR04014,no2,9.5,µg/m³ +Paris,FR,2019-06-10 13:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-10 12:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-10 11:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-06-10 10:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-06-10 09:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-06-10 08:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-06-10 07:00:00+00:00,FR04014,no2,23.0,µg/m³ +Paris,FR,2019-06-10 06:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-06-10 05:00:00+00:00,FR04014,no2,21.3,µg/m³ +Paris,FR,2019-06-10 04:00:00+00:00,FR04014,no2,13.7,µg/m³ +Paris,FR,2019-06-10 03:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-06-10 02:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-06-10 01:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-06-10 00:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-06-09 23:00:00+00:00,FR04014,no2,39.9,µg/m³ +Paris,FR,2019-06-09 22:00:00+00:00,FR04014,no2,37.1,µg/m³ +Paris,FR,2019-06-09 21:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-06-09 20:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-06-09 19:00:00+00:00,FR04014,no2,30.6,µg/m³ +Paris,FR,2019-06-09 18:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-09 17:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-06-09 16:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-06-09 15:00:00+00:00,FR04014,no2,7.2,µg/m³ +Paris,FR,2019-06-09 14:00:00+00:00,FR04014,no2,7.9,µg/m³ +Paris,FR,2019-06-09 13:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-09 12:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-06-09 11:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-06-09 10:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-06-09 09:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-06-09 08:00:00+00:00,FR04014,no2,30.2,µg/m³ +Paris,FR,2019-06-09 07:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-06-09 06:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-06-09 05:00:00+00:00,FR04014,no2,42.2,µg/m³ +Paris,FR,2019-06-09 04:00:00+00:00,FR04014,no2,43.0,µg/m³ +Paris,FR,2019-06-09 03:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-06-09 02:00:00+00:00,FR04014,no2,51.2,µg/m³ +Paris,FR,2019-06-09 01:00:00+00:00,FR04014,no2,41.0,µg/m³ +Paris,FR,2019-06-09 00:00:00+00:00,FR04014,no2,55.9,µg/m³ +Paris,FR,2019-06-08 23:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-06-08 22:00:00+00:00,FR04014,no2,34.8,µg/m³ +Paris,FR,2019-06-08 21:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-06-08 18:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-06-08 17:00:00+00:00,FR04014,no2,14.8,µg/m³ +Paris,FR,2019-06-08 16:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-06-08 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-08 14:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-06-08 13:00:00+00:00,FR04014,no2,11.1,µg/m³ +Paris,FR,2019-06-08 12:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-06-08 11:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-06-08 10:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-06-08 09:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-08 08:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-06-08 07:00:00+00:00,FR04014,no2,14.0,µg/m³ +Paris,FR,2019-06-08 06:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-06-08 05:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-06-08 04:00:00+00:00,FR04014,no2,10.7,µg/m³ +Paris,FR,2019-06-08 03:00:00+00:00,FR04014,no2,9.8,µg/m³ +Paris,FR,2019-06-08 02:00:00+00:00,FR04014,no2,8.4,µg/m³ +Paris,FR,2019-06-08 01:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-06-08 00:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-06-07 23:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-07 22:00:00+00:00,FR04014,no2,14.7,µg/m³ +Paris,FR,2019-06-07 21:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-06-07 20:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-06-07 19:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-06-07 18:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-06-07 17:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-07 16:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-07 15:00:00+00:00,FR04014,no2,15.6,µg/m³ +Paris,FR,2019-06-07 14:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-07 13:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-06-07 12:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-06-07 11:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-06-07 10:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-06-07 09:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-06-07 08:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-06-07 07:00:00+00:00,FR04014,no2,23.0,µg/m³ +Paris,FR,2019-06-07 06:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-06-06 14:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-06-06 13:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-06-06 12:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-06-06 11:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-06-06 10:00:00+00:00,FR04014,no2,21.2,µg/m³ +Paris,FR,2019-06-06 09:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-06-06 08:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-06-06 07:00:00+00:00,FR04014,no2,43.1,µg/m³ +Paris,FR,2019-06-06 06:00:00+00:00,FR04014,no2,40.5,µg/m³ +Paris,FR,2019-06-06 05:00:00+00:00,FR04014,no2,40.3,µg/m³ +Paris,FR,2019-06-06 04:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-06-06 03:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-06-06 02:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-06-06 01:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-06-06 00:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-06-05 23:00:00+00:00,FR04014,no2,31.8,µg/m³ +Paris,FR,2019-06-05 22:00:00+00:00,FR04014,no2,30.3,µg/m³ +Paris,FR,2019-06-05 21:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-06-05 20:00:00+00:00,FR04014,no2,37.5,µg/m³ +Paris,FR,2019-06-05 19:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-06-05 18:00:00+00:00,FR04014,no2,40.8,µg/m³ +Paris,FR,2019-06-05 17:00:00+00:00,FR04014,no2,48.8,µg/m³ +Paris,FR,2019-06-05 16:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-06-05 15:00:00+00:00,FR04014,no2,53.5,µg/m³ +Paris,FR,2019-06-05 14:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-06-05 13:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-06-05 12:00:00+00:00,FR04014,no2,47.2,µg/m³ +Paris,FR,2019-06-05 11:00:00+00:00,FR04014,no2,59.0,µg/m³ +Paris,FR,2019-06-05 10:00:00+00:00,FR04014,no2,42.1,µg/m³ +Paris,FR,2019-06-05 09:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-06-05 08:00:00+00:00,FR04014,no2,35.3,µg/m³ +Paris,FR,2019-06-05 07:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-06-05 06:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-06-05 05:00:00+00:00,FR04014,no2,39.2,µg/m³ +Paris,FR,2019-06-05 04:00:00+00:00,FR04014,no2,24.5,µg/m³ +Paris,FR,2019-06-05 03:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-06-05 02:00:00+00:00,FR04014,no2,12.4,µg/m³ +Paris,FR,2019-06-05 01:00:00+00:00,FR04014,no2,10.8,µg/m³ +Paris,FR,2019-06-05 00:00:00+00:00,FR04014,no2,15.7,µg/m³ +Paris,FR,2019-06-04 23:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-06-04 22:00:00+00:00,FR04014,no2,33.5,µg/m³ +Paris,FR,2019-06-04 21:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-06-04 20:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-06-04 19:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-06-04 18:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-06-04 17:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-06-04 16:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-06-04 15:00:00+00:00,FR04014,no2,21.5,µg/m³ +Paris,FR,2019-06-04 14:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-06-04 13:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-06-04 12:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-06-04 11:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-06-04 10:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-06-04 09:00:00+00:00,FR04014,no2,38.5,µg/m³ +Paris,FR,2019-06-04 08:00:00+00:00,FR04014,no2,50.8,µg/m³ +Paris,FR,2019-06-04 07:00:00+00:00,FR04014,no2,53.5,µg/m³ +Paris,FR,2019-06-04 06:00:00+00:00,FR04014,no2,47.7,µg/m³ +Paris,FR,2019-06-04 05:00:00+00:00,FR04014,no2,36.5,µg/m³ +Paris,FR,2019-06-04 04:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-06-04 03:00:00+00:00,FR04014,no2,41.6,µg/m³ +Paris,FR,2019-06-04 02:00:00+00:00,FR04014,no2,35.0,µg/m³ +Paris,FR,2019-06-04 01:00:00+00:00,FR04014,no2,43.9,µg/m³ +Paris,FR,2019-06-04 00:00:00+00:00,FR04014,no2,52.4,µg/m³ +Paris,FR,2019-06-03 23:00:00+00:00,FR04014,no2,44.6,µg/m³ +Paris,FR,2019-06-03 22:00:00+00:00,FR04014,no2,30.5,µg/m³ +Paris,FR,2019-06-03 21:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-06-03 20:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-06-03 19:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-06-03 18:00:00+00:00,FR04014,no2,23.1,µg/m³ +Paris,FR,2019-06-03 17:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-06-03 16:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-06-03 15:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-06-03 14:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-06-03 13:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-06-03 12:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-06-03 11:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-06-03 10:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-06-03 09:00:00+00:00,FR04014,no2,46.0,µg/m³ +Paris,FR,2019-06-03 08:00:00+00:00,FR04014,no2,43.9,µg/m³ +Paris,FR,2019-06-03 07:00:00+00:00,FR04014,no2,50.0,µg/m³ +Paris,FR,2019-06-03 06:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-06-03 05:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-06-03 04:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-06-03 03:00:00+00:00,FR04014,no2,9.8,µg/m³ +Paris,FR,2019-06-03 02:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-06-03 01:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-03 00:00:00+00:00,FR04014,no2,15.7,µg/m³ +Paris,FR,2019-06-02 23:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-06-02 22:00:00+00:00,FR04014,no2,27.6,µg/m³ +Paris,FR,2019-06-02 21:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-06-02 20:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-06-02 19:00:00+00:00,FR04014,no2,25.8,µg/m³ +Paris,FR,2019-06-02 18:00:00+00:00,FR04014,no2,15.6,µg/m³ +Paris,FR,2019-06-02 17:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-02 16:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-06-02 15:00:00+00:00,FR04014,no2,13.9,µg/m³ +Paris,FR,2019-06-02 14:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-06-02 13:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-06-02 12:00:00+00:00,FR04014,no2,11.5,µg/m³ +Paris,FR,2019-06-02 11:00:00+00:00,FR04014,no2,13.1,µg/m³ +Paris,FR,2019-06-02 10:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-06-02 09:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-06-02 08:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-06-02 07:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-06-02 06:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-06-02 05:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-06-02 04:00:00+00:00,FR04014,no2,24.5,µg/m³ +Paris,FR,2019-06-02 03:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-06-02 02:00:00+00:00,FR04014,no2,39.2,µg/m³ +Paris,FR,2019-06-02 01:00:00+00:00,FR04014,no2,38.2,µg/m³ +Paris,FR,2019-06-02 00:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-06-01 23:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-06-01 22:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-06-01 21:00:00+00:00,FR04014,no2,49.4,µg/m³ +Paris,FR,2019-06-01 20:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-06-01 19:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-06-01 18:00:00+00:00,FR04014,no2,14.5,µg/m³ +Paris,FR,2019-06-01 17:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-01 16:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-06-01 15:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-01 14:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-06-01 13:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-06-01 12:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-06-01 11:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-06-01 10:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-06-01 09:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-06-01 08:00:00+00:00,FR04014,no2,33.3,µg/m³ +Paris,FR,2019-06-01 07:00:00+00:00,FR04014,no2,46.4,µg/m³ +Paris,FR,2019-06-01 06:00:00+00:00,FR04014,no2,44.6,µg/m³ +Paris,FR,2019-06-01 02:00:00+00:00,FR04014,no2,68.1,µg/m³ +Paris,FR,2019-06-01 01:00:00+00:00,FR04014,no2,74.8,µg/m³ +Paris,FR,2019-06-01 00:00:00+00:00,FR04014,no2,84.7,µg/m³ +Paris,FR,2019-05-31 23:00:00+00:00,FR04014,no2,81.7,µg/m³ +Paris,FR,2019-05-31 22:00:00+00:00,FR04014,no2,68.0,µg/m³ +Paris,FR,2019-05-31 21:00:00+00:00,FR04014,no2,60.2,µg/m³ +Paris,FR,2019-05-31 20:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-31 19:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-31 18:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-31 17:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-31 16:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-05-31 15:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-31 14:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-31 13:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-05-31 12:00:00+00:00,FR04014,no2,13.3,µg/m³ +Paris,FR,2019-05-31 11:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-31 10:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-31 09:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-05-31 08:00:00+00:00,FR04014,no2,36.6,µg/m³ +Paris,FR,2019-05-31 07:00:00+00:00,FR04014,no2,47.4,µg/m³ +Paris,FR,2019-05-31 06:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-31 05:00:00+00:00,FR04014,no2,37.2,µg/m³ +Paris,FR,2019-05-31 04:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-05-31 03:00:00+00:00,FR04014,no2,40.1,µg/m³ +Paris,FR,2019-05-31 02:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-05-31 01:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-05-31 00:00:00+00:00,FR04014,no2,27.2,µg/m³ +Paris,FR,2019-05-30 23:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-05-30 22:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-05-30 21:00:00+00:00,FR04014,no2,26.9,µg/m³ +Paris,FR,2019-05-30 20:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-05-30 19:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-30 18:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-30 17:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-30 16:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-30 15:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-30 14:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-30 13:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-05-30 12:00:00+00:00,FR04014,no2,14.2,µg/m³ +Paris,FR,2019-05-30 11:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-30 10:00:00+00:00,FR04014,no2,13.8,µg/m³ +Paris,FR,2019-05-30 09:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-30 08:00:00+00:00,FR04014,no2,16.7,µg/m³ +Paris,FR,2019-05-30 07:00:00+00:00,FR04014,no2,18.3,µg/m³ +Paris,FR,2019-05-30 06:00:00+00:00,FR04014,no2,13.3,µg/m³ +Paris,FR,2019-05-30 05:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-05-30 04:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-05-30 03:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-30 02:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-05-30 01:00:00+00:00,FR04014,no2,12.4,µg/m³ +Paris,FR,2019-05-30 00:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-05-29 23:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-05-29 22:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-29 21:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-05-29 20:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-05-29 19:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-29 18:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-29 17:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-29 16:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-29 15:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-29 14:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-29 13:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-29 12:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-05-29 11:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-05-29 10:00:00+00:00,FR04014,no2,30.7,µg/m³ +Paris,FR,2019-05-29 09:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-29 08:00:00+00:00,FR04014,no2,45.7,µg/m³ +Paris,FR,2019-05-29 07:00:00+00:00,FR04014,no2,50.5,µg/m³ +Paris,FR,2019-05-29 06:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-29 05:00:00+00:00,FR04014,no2,36.7,µg/m³ +Paris,FR,2019-05-29 04:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-29 03:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-29 02:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-29 01:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-05-29 00:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-05-28 23:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-28 22:00:00+00:00,FR04014,no2,20.2,µg/m³ +Paris,FR,2019-05-28 21:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-28 20:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-28 19:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-28 18:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-05-28 17:00:00+00:00,FR04014,no2,20.8,µg/m³ +Paris,FR,2019-05-28 16:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-05-28 15:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-05-28 14:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-28 13:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-28 12:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-05-28 11:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-28 10:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-05-28 09:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-28 08:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-05-28 07:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-05-28 06:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-05-28 05:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-05-28 04:00:00+00:00,FR04014,no2,8.9,µg/m³ +Paris,FR,2019-05-28 03:00:00+00:00,FR04014,no2,6.1,µg/m³ +Paris,FR,2019-05-28 02:00:00+00:00,FR04014,no2,6.4,µg/m³ +Paris,FR,2019-05-28 01:00:00+00:00,FR04014,no2,8.2,µg/m³ +Paris,FR,2019-05-28 00:00:00+00:00,FR04014,no2,15.4,µg/m³ +Paris,FR,2019-05-27 23:00:00+00:00,FR04014,no2,22.6,µg/m³ +Paris,FR,2019-05-27 22:00:00+00:00,FR04014,no2,19.9,µg/m³ +Paris,FR,2019-05-27 21:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-27 20:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-27 19:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-27 18:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-05-27 17:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-27 16:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-05-27 15:00:00+00:00,FR04014,no2,25.6,µg/m³ +Paris,FR,2019-05-27 14:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-05-27 13:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-27 12:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-05-27 11:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-05-27 10:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-27 09:00:00+00:00,FR04014,no2,31.4,µg/m³ +Paris,FR,2019-05-27 08:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-05-27 07:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-05-27 06:00:00+00:00,FR04014,no2,29.1,µg/m³ +Paris,FR,2019-05-27 05:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-27 04:00:00+00:00,FR04014,no2,6.5,µg/m³ +Paris,FR,2019-05-27 03:00:00+00:00,FR04014,no2,4.8,µg/m³ +Paris,FR,2019-05-27 02:00:00+00:00,FR04014,no2,5.9,µg/m³ +Paris,FR,2019-05-27 01:00:00+00:00,FR04014,no2,7.1,µg/m³ +Paris,FR,2019-05-27 00:00:00+00:00,FR04014,no2,9.5,µg/m³ +Paris,FR,2019-05-26 23:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-26 22:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-05-26 21:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-05-26 20:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-05-26 19:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-26 18:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-05-26 17:00:00+00:00,FR04014,no2,17.3,µg/m³ +Paris,FR,2019-05-26 16:00:00+00:00,FR04014,no2,17.1,µg/m³ +Paris,FR,2019-05-26 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-26 14:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-26 13:00:00+00:00,FR04014,no2,12.5,µg/m³ +Paris,FR,2019-05-26 12:00:00+00:00,FR04014,no2,11.5,µg/m³ +Paris,FR,2019-05-26 11:00:00+00:00,FR04014,no2,13.3,µg/m³ +Paris,FR,2019-05-26 10:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-05-26 09:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-26 08:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-05-26 07:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-05-26 06:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-26 05:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-05-26 04:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-26 03:00:00+00:00,FR04014,no2,22.9,µg/m³ +Paris,FR,2019-05-26 02:00:00+00:00,FR04014,no2,23.4,µg/m³ +Paris,FR,2019-05-26 01:00:00+00:00,FR04014,no2,49.8,µg/m³ +Paris,FR,2019-05-26 00:00:00+00:00,FR04014,no2,67.0,µg/m³ +Paris,FR,2019-05-25 23:00:00+00:00,FR04014,no2,70.2,µg/m³ +Paris,FR,2019-05-25 22:00:00+00:00,FR04014,no2,63.9,µg/m³ +Paris,FR,2019-05-25 21:00:00+00:00,FR04014,no2,39.5,µg/m³ +Paris,FR,2019-05-25 20:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-05-25 19:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-25 18:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-05-25 17:00:00+00:00,FR04014,no2,20.6,µg/m³ +Paris,FR,2019-05-25 16:00:00+00:00,FR04014,no2,31.9,µg/m³ +Paris,FR,2019-05-25 15:00:00+00:00,FR04014,no2,30.0,µg/m³ +Paris,FR,2019-05-25 14:00:00+00:00,FR04014,no2,23.6,µg/m³ +Paris,FR,2019-05-25 13:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-25 12:00:00+00:00,FR04014,no2,18.6,µg/m³ +Paris,FR,2019-05-25 11:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-25 10:00:00+00:00,FR04014,no2,26.3,µg/m³ +Paris,FR,2019-05-25 09:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-05-25 08:00:00+00:00,FR04014,no2,44.5,µg/m³ +Paris,FR,2019-05-25 07:00:00+00:00,FR04014,no2,42.1,µg/m³ +Paris,FR,2019-05-25 06:00:00+00:00,FR04014,no2,36.9,µg/m³ +Paris,FR,2019-05-25 02:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-25 01:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-25 00:00:00+00:00,FR04014,no2,17.4,µg/m³ +Paris,FR,2019-05-24 23:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-05-24 22:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-05-24 21:00:00+00:00,FR04014,no2,18.1,µg/m³ +Paris,FR,2019-05-24 20:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-05-24 19:00:00+00:00,FR04014,no2,21.9,µg/m³ +Paris,FR,2019-05-24 18:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-24 17:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-24 16:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-05-24 15:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-24 14:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-24 13:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-24 12:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-05-24 11:00:00+00:00,FR04014,no2,40.6,µg/m³ +Paris,FR,2019-05-24 10:00:00+00:00,FR04014,no2,28.6,µg/m³ +Paris,FR,2019-05-24 09:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-05-24 08:00:00+00:00,FR04014,no2,45.9,µg/m³ +Paris,FR,2019-05-24 07:00:00+00:00,FR04014,no2,54.8,µg/m³ +Paris,FR,2019-05-24 06:00:00+00:00,FR04014,no2,40.7,µg/m³ +Paris,FR,2019-05-24 05:00:00+00:00,FR04014,no2,35.9,µg/m³ +Paris,FR,2019-05-24 04:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-05-24 03:00:00+00:00,FR04014,no2,19.4,µg/m³ +Paris,FR,2019-05-24 02:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-24 01:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-05-24 00:00:00+00:00,FR04014,no2,32.8,µg/m³ +Paris,FR,2019-05-23 23:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-23 22:00:00+00:00,FR04014,no2,61.9,µg/m³ +Paris,FR,2019-05-23 21:00:00+00:00,FR04014,no2,47.0,µg/m³ +Paris,FR,2019-05-23 20:00:00+00:00,FR04014,no2,33.8,µg/m³ +Paris,FR,2019-05-23 19:00:00+00:00,FR04014,no2,28.0,µg/m³ +Paris,FR,2019-05-23 18:00:00+00:00,FR04014,no2,23.5,µg/m³ +Paris,FR,2019-05-23 17:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-23 16:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-23 15:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-23 14:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-23 13:00:00+00:00,FR04014,no2,21.2,µg/m³ +Paris,FR,2019-05-23 12:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-05-23 11:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-05-23 10:00:00+00:00,FR04014,no2,28.3,µg/m³ +Paris,FR,2019-05-23 09:00:00+00:00,FR04014,no2,79.4,µg/m³ +Paris,FR,2019-05-23 08:00:00+00:00,FR04014,no2,97.0,µg/m³ +Paris,FR,2019-05-23 07:00:00+00:00,FR04014,no2,91.8,µg/m³ +Paris,FR,2019-05-23 06:00:00+00:00,FR04014,no2,79.6,µg/m³ +Paris,FR,2019-05-23 05:00:00+00:00,FR04014,no2,68.7,µg/m³ +Paris,FR,2019-05-23 04:00:00+00:00,FR04014,no2,71.9,µg/m³ +Paris,FR,2019-05-23 03:00:00+00:00,FR04014,no2,76.8,µg/m³ +Paris,FR,2019-05-23 02:00:00+00:00,FR04014,no2,66.6,µg/m³ +Paris,FR,2019-05-23 01:00:00+00:00,FR04014,no2,53.1,µg/m³ +Paris,FR,2019-05-23 00:00:00+00:00,FR04014,no2,53.3,µg/m³ +Paris,FR,2019-05-22 23:00:00+00:00,FR04014,no2,62.1,µg/m³ +Paris,FR,2019-05-22 22:00:00+00:00,FR04014,no2,29.8,µg/m³ +Paris,FR,2019-05-22 21:00:00+00:00,FR04014,no2,37.7,µg/m³ +Paris,FR,2019-05-22 20:00:00+00:00,FR04014,no2,44.9,µg/m³ +Paris,FR,2019-05-22 19:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-05-22 18:00:00+00:00,FR04014,no2,34.1,µg/m³ +Paris,FR,2019-05-22 17:00:00+00:00,FR04014,no2,36.1,µg/m³ +Paris,FR,2019-05-22 16:00:00+00:00,FR04014,no2,34.9,µg/m³ +Paris,FR,2019-05-22 15:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-05-22 14:00:00+00:00,FR04014,no2,40.0,µg/m³ +Paris,FR,2019-05-22 13:00:00+00:00,FR04014,no2,38.5,µg/m³ +Paris,FR,2019-05-22 12:00:00+00:00,FR04014,no2,42.2,µg/m³ +Paris,FR,2019-05-22 11:00:00+00:00,FR04014,no2,42.6,µg/m³ +Paris,FR,2019-05-22 10:00:00+00:00,FR04014,no2,57.8,µg/m³ +Paris,FR,2019-05-22 09:00:00+00:00,FR04014,no2,63.1,µg/m³ +Paris,FR,2019-05-22 08:00:00+00:00,FR04014,no2,70.8,µg/m³ +Paris,FR,2019-05-22 07:00:00+00:00,FR04014,no2,75.4,µg/m³ +Paris,FR,2019-05-22 06:00:00+00:00,FR04014,no2,75.7,µg/m³ +Paris,FR,2019-05-22 05:00:00+00:00,FR04014,no2,45.1,µg/m³ +Paris,FR,2019-05-22 04:00:00+00:00,FR04014,no2,33.7,µg/m³ +Paris,FR,2019-05-22 03:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-22 02:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-05-22 01:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-22 00:00:00+00:00,FR04014,no2,27.1,µg/m³ +Paris,FR,2019-05-21 23:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-05-21 22:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-05-21 21:00:00+00:00,FR04014,no2,43.0,µg/m³ +Paris,FR,2019-05-21 20:00:00+00:00,FR04014,no2,40.8,µg/m³ +Paris,FR,2019-05-21 19:00:00+00:00,FR04014,no2,50.0,µg/m³ +Paris,FR,2019-05-21 18:00:00+00:00,FR04014,no2,54.3,µg/m³ +Paris,FR,2019-05-21 17:00:00+00:00,FR04014,no2,75.0,µg/m³ +Paris,FR,2019-05-21 16:00:00+00:00,FR04014,no2,42.3,µg/m³ +Paris,FR,2019-05-21 15:00:00+00:00,FR04014,no2,36.6,µg/m³ +Paris,FR,2019-05-21 14:00:00+00:00,FR04014,no2,47.8,µg/m³ +Paris,FR,2019-05-21 13:00:00+00:00,FR04014,no2,49.7,µg/m³ +Paris,FR,2019-05-21 12:00:00+00:00,FR04014,no2,30.5,µg/m³ +Paris,FR,2019-05-21 11:00:00+00:00,FR04014,no2,25.5,µg/m³ +Paris,FR,2019-05-21 10:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-05-21 09:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-05-21 08:00:00+00:00,FR04014,no2,54.2,µg/m³ +Paris,FR,2019-05-21 07:00:00+00:00,FR04014,no2,56.0,µg/m³ +Paris,FR,2019-05-21 06:00:00+00:00,FR04014,no2,62.6,µg/m³ +Paris,FR,2019-05-21 05:00:00+00:00,FR04014,no2,38.0,µg/m³ +Paris,FR,2019-05-21 04:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-21 03:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-05-21 02:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-05-21 01:00:00+00:00,FR04014,no2,16.3,µg/m³ +Paris,FR,2019-05-21 00:00:00+00:00,FR04014,no2,16.9,µg/m³ +Paris,FR,2019-05-20 23:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-05-20 22:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-05-20 21:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-20 20:00:00+00:00,FR04014,no2,21.6,µg/m³ +Paris,FR,2019-05-20 19:00:00+00:00,FR04014,no2,21.3,µg/m³ +Paris,FR,2019-05-20 18:00:00+00:00,FR04014,no2,32.2,µg/m³ +Paris,FR,2019-05-20 17:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-20 16:00:00+00:00,FR04014,no2,32.4,µg/m³ +Paris,FR,2019-05-20 15:00:00+00:00,FR04014,no2,26.5,µg/m³ +Paris,FR,2019-05-20 14:00:00+00:00,FR04014,no2,27.5,µg/m³ +Paris,FR,2019-05-20 13:00:00+00:00,FR04014,no2,23.7,µg/m³ +Paris,FR,2019-05-20 12:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-20 11:00:00+00:00,FR04014,no2,35.4,µg/m³ +Paris,FR,2019-05-20 10:00:00+00:00,FR04014,no2,43.9,µg/m³ +Paris,FR,2019-05-20 09:00:00+00:00,FR04014,no2,45.5,µg/m³ +Paris,FR,2019-05-20 08:00:00+00:00,FR04014,no2,46.1,µg/m³ +Paris,FR,2019-05-20 07:00:00+00:00,FR04014,no2,46.9,µg/m³ +Paris,FR,2019-05-20 06:00:00+00:00,FR04014,no2,40.1,µg/m³ +Paris,FR,2019-05-20 05:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-20 04:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-20 03:00:00+00:00,FR04014,no2,12.6,µg/m³ +Paris,FR,2019-05-20 02:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-05-20 01:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-20 00:00:00+00:00,FR04014,no2,16.4,µg/m³ +Paris,FR,2019-05-19 23:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-19 22:00:00+00:00,FR04014,no2,22.2,µg/m³ +Paris,FR,2019-05-19 21:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-19 20:00:00+00:00,FR04014,no2,35.6,µg/m³ +Paris,FR,2019-05-19 19:00:00+00:00,FR04014,no2,51.2,µg/m³ +Paris,FR,2019-05-19 18:00:00+00:00,FR04014,no2,32.7,µg/m³ +Paris,FR,2019-05-19 17:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-05-19 16:00:00+00:00,FR04014,no2,32.5,µg/m³ +Paris,FR,2019-05-19 15:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-05-19 14:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-19 13:00:00+00:00,FR04014,no2,21.0,µg/m³ +Paris,FR,2019-05-19 12:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-19 11:00:00+00:00,FR04014,no2,32.6,µg/m³ +Paris,FR,2019-05-19 10:00:00+00:00,FR04014,no2,31.0,µg/m³ +Paris,FR,2019-05-19 09:00:00+00:00,FR04014,no2,33.0,µg/m³ +Paris,FR,2019-05-19 08:00:00+00:00,FR04014,no2,31.7,µg/m³ +Paris,FR,2019-05-19 07:00:00+00:00,FR04014,no2,32.4,µg/m³ +Paris,FR,2019-05-19 06:00:00+00:00,FR04014,no2,31.1,µg/m³ +Paris,FR,2019-05-19 05:00:00+00:00,FR04014,no2,40.9,µg/m³ +Paris,FR,2019-05-19 04:00:00+00:00,FR04014,no2,39.4,µg/m³ +Paris,FR,2019-05-19 03:00:00+00:00,FR04014,no2,36.4,µg/m³ +Paris,FR,2019-05-19 02:00:00+00:00,FR04014,no2,38.1,µg/m³ +Paris,FR,2019-05-19 01:00:00+00:00,FR04014,no2,34.9,µg/m³ +Paris,FR,2019-05-19 00:00:00+00:00,FR04014,no2,49.6,µg/m³ +Paris,FR,2019-05-18 23:00:00+00:00,FR04014,no2,50.2,µg/m³ +Paris,FR,2019-05-18 22:00:00+00:00,FR04014,no2,62.5,µg/m³ +Paris,FR,2019-05-18 21:00:00+00:00,FR04014,no2,59.3,µg/m³ +Paris,FR,2019-05-18 20:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-05-18 19:00:00+00:00,FR04014,no2,67.5,µg/m³ +Paris,FR,2019-05-18 18:00:00+00:00,FR04014,no2,14.5,µg/m³ +Paris,FR,2019-05-18 17:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-18 16:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-05-18 15:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-18 14:00:00+00:00,FR04014,no2,11.8,µg/m³ +Paris,FR,2019-05-18 13:00:00+00:00,FR04014,no2,10.5,µg/m³ +Paris,FR,2019-05-18 12:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-18 11:00:00+00:00,FR04014,no2,17.5,µg/m³ +Paris,FR,2019-05-18 10:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-18 09:00:00+00:00,FR04014,no2,21.1,µg/m³ +Paris,FR,2019-05-18 08:00:00+00:00,FR04014,no2,20.4,µg/m³ +Paris,FR,2019-05-18 07:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-05-18 06:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-18 05:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-18 04:00:00+00:00,FR04014,no2,16.6,µg/m³ +Paris,FR,2019-05-18 03:00:00+00:00,FR04014,no2,16.1,µg/m³ +Paris,FR,2019-05-18 02:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-18 01:00:00+00:00,FR04014,no2,37.4,µg/m³ +Paris,FR,2019-05-18 00:00:00+00:00,FR04014,no2,31.5,µg/m³ +Paris,FR,2019-05-17 23:00:00+00:00,FR04014,no2,34.1,µg/m³ +Paris,FR,2019-05-17 22:00:00+00:00,FR04014,no2,28.2,µg/m³ +Paris,FR,2019-05-17 21:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-17 20:00:00+00:00,FR04014,no2,23.5,µg/m³ +Paris,FR,2019-05-17 19:00:00+00:00,FR04014,no2,24.7,µg/m³ +Paris,FR,2019-05-17 18:00:00+00:00,FR04014,no2,33.6,µg/m³ +Paris,FR,2019-05-17 17:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-17 16:00:00+00:00,FR04014,no2,20.7,µg/m³ +Paris,FR,2019-05-17 15:00:00+00:00,FR04014,no2,22.2,µg/m³ +Paris,FR,2019-05-17 14:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-05-17 13:00:00+00:00,FR04014,no2,37.9,µg/m³ +Paris,FR,2019-05-17 12:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-17 11:00:00+00:00,FR04014,no2,43.1,µg/m³ +Paris,FR,2019-05-17 10:00:00+00:00,FR04014,no2,51.5,µg/m³ +Paris,FR,2019-05-17 09:00:00+00:00,FR04014,no2,60.5,µg/m³ +Paris,FR,2019-05-17 08:00:00+00:00,FR04014,no2,57.5,µg/m³ +Paris,FR,2019-05-17 07:00:00+00:00,FR04014,no2,55.0,µg/m³ +Paris,FR,2019-05-17 06:00:00+00:00,FR04014,no2,46.3,µg/m³ +Paris,FR,2019-05-17 05:00:00+00:00,FR04014,no2,34.0,µg/m³ +Paris,FR,2019-05-17 04:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-17 03:00:00+00:00,FR04014,no2,26.6,µg/m³ +Paris,FR,2019-05-17 02:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-17 01:00:00+00:00,FR04014,no2,26.1,µg/m³ +Paris,FR,2019-05-17 00:00:00+00:00,FR04014,no2,46.3,µg/m³ +Paris,FR,2019-05-16 23:00:00+00:00,FR04014,no2,43.7,µg/m³ +Paris,FR,2019-05-16 22:00:00+00:00,FR04014,no2,37.1,µg/m³ +Paris,FR,2019-05-16 21:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-16 20:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-05-16 19:00:00+00:00,FR04014,no2,14.4,µg/m³ +Paris,FR,2019-05-16 18:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-05-16 17:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-05-16 16:00:00+00:00,FR04014,no2,10.3,µg/m³ +Paris,FR,2019-05-16 15:00:00+00:00,FR04014,no2,10.1,µg/m³ +Paris,FR,2019-05-16 14:00:00+00:00,FR04014,no2,8.1,µg/m³ +Paris,FR,2019-05-16 13:00:00+00:00,FR04014,no2,8.5,µg/m³ +Paris,FR,2019-05-16 12:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-05-16 11:00:00+00:00,FR04014,no2,10.5,µg/m³ +Paris,FR,2019-05-16 10:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-05-16 09:00:00+00:00,FR04014,no2,29.5,µg/m³ +Paris,FR,2019-05-16 08:00:00+00:00,FR04014,no2,39.4,µg/m³ +Paris,FR,2019-05-16 07:00:00+00:00,FR04014,no2,40.0,µg/m³ +Paris,FR,2019-05-16 05:00:00+00:00,FR04014,no2,52.6,µg/m³ +Paris,FR,2019-05-16 04:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-16 03:00:00+00:00,FR04014,no2,27.9,µg/m³ +Paris,FR,2019-05-16 02:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-05-16 01:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-05-16 00:00:00+00:00,FR04014,no2,27.4,µg/m³ +Paris,FR,2019-05-15 23:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-05-15 22:00:00+00:00,FR04014,no2,44.1,µg/m³ +Paris,FR,2019-05-15 21:00:00+00:00,FR04014,no2,36.0,µg/m³ +Paris,FR,2019-05-15 20:00:00+00:00,FR04014,no2,30.1,µg/m³ +Paris,FR,2019-05-15 19:00:00+00:00,FR04014,no2,20.3,µg/m³ +Paris,FR,2019-05-15 18:00:00+00:00,FR04014,no2,16.5,µg/m³ +Paris,FR,2019-05-15 17:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-15 16:00:00+00:00,FR04014,no2,12.2,µg/m³ +Paris,FR,2019-05-15 15:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-15 14:00:00+00:00,FR04014,no2,11.9,µg/m³ +Paris,FR,2019-05-15 13:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-05-15 12:00:00+00:00,FR04014,no2,9.4,µg/m³ +Paris,FR,2019-05-15 11:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-15 10:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-15 09:00:00+00:00,FR04014,no2,0.0,µg/m³ +Paris,FR,2019-05-15 08:00:00+00:00,FR04014,no2,25.7,µg/m³ +Paris,FR,2019-05-15 07:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-05-15 06:00:00+00:00,FR04014,no2,48.1,µg/m³ +Paris,FR,2019-05-15 05:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-15 04:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-05-15 03:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-05-15 02:00:00+00:00,FR04014,no2,16.8,µg/m³ +Paris,FR,2019-05-15 01:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-15 00:00:00+00:00,FR04014,no2,18.8,µg/m³ +Paris,FR,2019-05-14 23:00:00+00:00,FR04014,no2,24.3,µg/m³ +Paris,FR,2019-05-14 22:00:00+00:00,FR04014,no2,30.9,µg/m³ +Paris,FR,2019-05-14 21:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-14 20:00:00+00:00,FR04014,no2,28.4,µg/m³ +Paris,FR,2019-05-14 19:00:00+00:00,FR04014,no2,23.3,µg/m³ +Paris,FR,2019-05-14 18:00:00+00:00,FR04014,no2,17.9,µg/m³ +Paris,FR,2019-05-14 17:00:00+00:00,FR04014,no2,17.7,µg/m³ +Paris,FR,2019-05-14 16:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-14 15:00:00+00:00,FR04014,no2,13.4,µg/m³ +Paris,FR,2019-05-14 14:00:00+00:00,FR04014,no2,15.2,µg/m³ +Paris,FR,2019-05-14 13:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-05-14 12:00:00+00:00,FR04014,no2,10.2,µg/m³ +Paris,FR,2019-05-14 11:00:00+00:00,FR04014,no2,11.3,µg/m³ +Paris,FR,2019-05-14 10:00:00+00:00,FR04014,no2,12.9,µg/m³ +Paris,FR,2019-05-14 09:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-14 08:00:00+00:00,FR04014,no2,28.8,µg/m³ +Paris,FR,2019-05-14 07:00:00+00:00,FR04014,no2,41.3,µg/m³ +Paris,FR,2019-05-14 06:00:00+00:00,FR04014,no2,46.1,µg/m³ +Paris,FR,2019-05-14 05:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-14 04:00:00+00:00,FR04014,no2,31.6,µg/m³ +Paris,FR,2019-05-14 03:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-14 02:00:00+00:00,FR04014,no2,19.0,µg/m³ +Paris,FR,2019-05-14 01:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-05-14 00:00:00+00:00,FR04014,no2,20.9,µg/m³ +Paris,FR,2019-05-13 23:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-05-13 22:00:00+00:00,FR04014,no2,27.3,µg/m³ +Paris,FR,2019-05-13 21:00:00+00:00,FR04014,no2,30.4,µg/m³ +Paris,FR,2019-05-13 20:00:00+00:00,FR04014,no2,28.3,µg/m³ +Paris,FR,2019-05-13 19:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-05-13 18:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-05-13 17:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-13 16:00:00+00:00,FR04014,no2,12.1,µg/m³ +Paris,FR,2019-05-13 15:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-13 14:00:00+00:00,FR04014,no2,10.7,µg/m³ +Paris,FR,2019-05-13 13:00:00+00:00,FR04014,no2,10.1,µg/m³ +Paris,FR,2019-05-13 12:00:00+00:00,FR04014,no2,9.2,µg/m³ +Paris,FR,2019-05-13 11:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-05-13 10:00:00+00:00,FR04014,no2,12.8,µg/m³ +Paris,FR,2019-05-13 09:00:00+00:00,FR04014,no2,20.6,µg/m³ +Paris,FR,2019-05-13 08:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-05-13 07:00:00+00:00,FR04014,no2,41.0,µg/m³ +Paris,FR,2019-05-13 06:00:00+00:00,FR04014,no2,45.2,µg/m³ +Paris,FR,2019-05-13 05:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-05-13 04:00:00+00:00,FR04014,no2,25.1,µg/m³ +Paris,FR,2019-05-13 03:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-05-13 02:00:00+00:00,FR04014,no2,18.5,µg/m³ +Paris,FR,2019-05-13 01:00:00+00:00,FR04014,no2,18.9,µg/m³ +Paris,FR,2019-05-13 00:00:00+00:00,FR04014,no2,25.0,µg/m³ +Paris,FR,2019-05-12 23:00:00+00:00,FR04014,no2,32.5,µg/m³ +Paris,FR,2019-05-12 22:00:00+00:00,FR04014,no2,46.5,µg/m³ +Paris,FR,2019-05-12 21:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-05-12 20:00:00+00:00,FR04014,no2,24.1,µg/m³ +Paris,FR,2019-05-12 19:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-12 18:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-05-12 17:00:00+00:00,FR04014,no2,13.9,µg/m³ +Paris,FR,2019-05-12 16:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-12 15:00:00+00:00,FR04014,no2,9.6,µg/m³ +Paris,FR,2019-05-12 14:00:00+00:00,FR04014,no2,9.1,µg/m³ +Paris,FR,2019-05-12 13:00:00+00:00,FR04014,no2,8.7,µg/m³ +Paris,FR,2019-05-12 12:00:00+00:00,FR04014,no2,10.9,µg/m³ +Paris,FR,2019-05-12 11:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-05-12 10:00:00+00:00,FR04014,no2,11.4,µg/m³ +Paris,FR,2019-05-12 09:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-12 08:00:00+00:00,FR04014,no2,14.6,µg/m³ +Paris,FR,2019-05-12 07:00:00+00:00,FR04014,no2,15.9,µg/m³ +Paris,FR,2019-05-12 06:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-12 05:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-05-12 04:00:00+00:00,FR04014,no2,16.2,µg/m³ +Paris,FR,2019-05-12 03:00:00+00:00,FR04014,no2,16.0,µg/m³ +Paris,FR,2019-05-12 02:00:00+00:00,FR04014,no2,17.2,µg/m³ +Paris,FR,2019-05-12 01:00:00+00:00,FR04014,no2,19.2,µg/m³ +Paris,FR,2019-05-12 00:00:00+00:00,FR04014,no2,22.8,µg/m³ +Paris,FR,2019-05-11 23:00:00+00:00,FR04014,no2,26.4,µg/m³ +Paris,FR,2019-05-11 22:00:00+00:00,FR04014,no2,27.7,µg/m³ +Paris,FR,2019-05-11 21:00:00+00:00,FR04014,no2,21.1,µg/m³ +Paris,FR,2019-05-11 20:00:00+00:00,FR04014,no2,24.2,µg/m³ +Paris,FR,2019-05-11 19:00:00+00:00,FR04014,no2,31.2,µg/m³ +Paris,FR,2019-05-11 18:00:00+00:00,FR04014,no2,33.1,µg/m³ +Paris,FR,2019-05-11 17:00:00+00:00,FR04014,no2,32.0,µg/m³ +Paris,FR,2019-05-11 16:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-11 15:00:00+00:00,FR04014,no2,18.0,µg/m³ +Paris,FR,2019-05-11 14:00:00+00:00,FR04014,no2,17.8,µg/m³ +Paris,FR,2019-05-11 13:00:00+00:00,FR04014,no2,30.8,µg/m³ +Paris,FR,2019-05-11 12:00:00+00:00,FR04014,no2,30.2,µg/m³ +Paris,FR,2019-05-11 11:00:00+00:00,FR04014,no2,33.2,µg/m³ +Paris,FR,2019-05-11 10:00:00+00:00,FR04014,no2,36.8,µg/m³ +Paris,FR,2019-05-11 09:00:00+00:00,FR04014,no2,35.7,µg/m³ +Paris,FR,2019-05-11 08:00:00+00:00,FR04014,no2,32.1,µg/m³ +Paris,FR,2019-05-11 07:00:00+00:00,FR04014,no2,29.0,µg/m³ +Paris,FR,2019-05-11 06:00:00+00:00,FR04014,no2,28.9,µg/m³ +Paris,FR,2019-05-11 02:00:00+00:00,FR04014,no2,14.9,µg/m³ +Paris,FR,2019-05-11 01:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-05-11 00:00:00+00:00,FR04014,no2,24.8,µg/m³ +Paris,FR,2019-05-10 23:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-05-10 22:00:00+00:00,FR04014,no2,28.1,µg/m³ +Paris,FR,2019-05-10 21:00:00+00:00,FR04014,no2,37.0,µg/m³ +Paris,FR,2019-05-10 20:00:00+00:00,FR04014,no2,43.6,µg/m³ +Paris,FR,2019-05-10 19:00:00+00:00,FR04014,no2,39.3,µg/m³ +Paris,FR,2019-05-10 18:00:00+00:00,FR04014,no2,33.4,µg/m³ +Paris,FR,2019-05-10 17:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-05-10 16:00:00+00:00,FR04014,no2,30.8,µg/m³ +Paris,FR,2019-05-10 15:00:00+00:00,FR04014,no2,29.6,µg/m³ +Paris,FR,2019-05-10 14:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-05-10 13:00:00+00:00,FR04014,no2,22.0,µg/m³ +Paris,FR,2019-05-10 12:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-10 11:00:00+00:00,FR04014,no2,23.2,µg/m³ +Paris,FR,2019-05-10 10:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-05-10 09:00:00+00:00,FR04014,no2,53.4,µg/m³ +Paris,FR,2019-05-10 08:00:00+00:00,FR04014,no2,60.7,µg/m³ +Paris,FR,2019-05-10 07:00:00+00:00,FR04014,no2,57.3,µg/m³ +Paris,FR,2019-05-10 06:00:00+00:00,FR04014,no2,47.4,µg/m³ +Paris,FR,2019-05-10 05:00:00+00:00,FR04014,no2,37.8,µg/m³ +Paris,FR,2019-05-10 04:00:00+00:00,FR04014,no2,20.5,µg/m³ +Paris,FR,2019-05-10 03:00:00+00:00,FR04014,no2,15.0,µg/m³ +Paris,FR,2019-05-10 02:00:00+00:00,FR04014,no2,14.1,µg/m³ +Paris,FR,2019-05-10 01:00:00+00:00,FR04014,no2,19.1,µg/m³ +Paris,FR,2019-05-10 00:00:00+00:00,FR04014,no2,22.7,µg/m³ +Paris,FR,2019-05-09 23:00:00+00:00,FR04014,no2,26.7,µg/m³ +Paris,FR,2019-05-09 22:00:00+00:00,FR04014,no2,29.7,µg/m³ +Paris,FR,2019-05-09 21:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-09 20:00:00+00:00,FR04014,no2,29.2,µg/m³ +Paris,FR,2019-05-09 19:00:00+00:00,FR04014,no2,23.8,µg/m³ +Paris,FR,2019-05-09 18:00:00+00:00,FR04014,no2,24.4,µg/m³ +Paris,FR,2019-05-09 17:00:00+00:00,FR04014,no2,29.9,µg/m³ +Paris,FR,2019-05-09 16:00:00+00:00,FR04014,no2,27.0,µg/m³ +Paris,FR,2019-05-09 15:00:00+00:00,FR04014,no2,23.9,µg/m³ +Paris,FR,2019-05-09 14:00:00+00:00,FR04014,no2,24.6,µg/m³ +Paris,FR,2019-05-09 13:00:00+00:00,FR04014,no2,21.3,µg/m³ +Paris,FR,2019-05-09 12:00:00+00:00,FR04014,no2,35.1,µg/m³ +Paris,FR,2019-05-09 11:00:00+00:00,FR04014,no2,34.2,µg/m³ +Paris,FR,2019-05-09 10:00:00+00:00,FR04014,no2,43.1,µg/m³ +Paris,FR,2019-05-09 09:00:00+00:00,FR04014,no2,32.3,µg/m³ +Paris,FR,2019-05-09 08:00:00+00:00,FR04014,no2,32.2,µg/m³ +Paris,FR,2019-05-09 07:00:00+00:00,FR04014,no2,49.0,µg/m³ +Paris,FR,2019-05-09 06:00:00+00:00,FR04014,no2,50.7,µg/m³ +Paris,FR,2019-05-09 05:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-09 04:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-09 03:00:00+00:00,FR04014,no2,10.4,µg/m³ +Paris,FR,2019-05-09 02:00:00+00:00,FR04014,no2,10.0,µg/m³ +Paris,FR,2019-05-09 01:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-09 00:00:00+00:00,FR04014,no2,14.7,µg/m³ +Paris,FR,2019-05-08 23:00:00+00:00,FR04014,no2,25.2,µg/m³ +Paris,FR,2019-05-08 22:00:00+00:00,FR04014,no2,32.2,µg/m³ +Paris,FR,2019-05-08 21:00:00+00:00,FR04014,no2,48.9,µg/m³ +Paris,FR,2019-05-08 20:00:00+00:00,FR04014,no2,38.3,µg/m³ +Paris,FR,2019-05-08 19:00:00+00:00,FR04014,no2,41.3,µg/m³ +Paris,FR,2019-05-08 18:00:00+00:00,FR04014,no2,27.8,µg/m³ +Paris,FR,2019-05-08 17:00:00+00:00,FR04014,no2,29.3,µg/m³ +Paris,FR,2019-05-08 16:00:00+00:00,FR04014,no2,38.6,µg/m³ +Paris,FR,2019-05-08 15:00:00+00:00,FR04014,no2,26.0,µg/m³ +Paris,FR,2019-05-08 14:00:00+00:00,FR04014,no2,25.3,µg/m³ +Paris,FR,2019-05-08 13:00:00+00:00,FR04014,no2,14.3,µg/m³ +Paris,FR,2019-05-08 12:00:00+00:00,FR04014,no2,15.1,µg/m³ +Paris,FR,2019-05-08 11:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-05-08 10:00:00+00:00,FR04014,no2,33.4,µg/m³ +Paris,FR,2019-05-08 09:00:00+00:00,FR04014,no2,19.7,µg/m³ +Paris,FR,2019-05-08 08:00:00+00:00,FR04014,no2,17.0,µg/m³ +Paris,FR,2019-05-08 07:00:00+00:00,FR04014,no2,19.5,µg/m³ +Paris,FR,2019-05-08 06:00:00+00:00,FR04014,no2,21.7,µg/m³ +Paris,FR,2019-05-08 05:00:00+00:00,FR04014,no2,19.3,µg/m³ +Paris,FR,2019-05-08 04:00:00+00:00,FR04014,no2,15.5,µg/m³ +Paris,FR,2019-05-08 03:00:00+00:00,FR04014,no2,13.5,µg/m³ +Paris,FR,2019-05-08 02:00:00+00:00,FR04014,no2,15.3,µg/m³ +Paris,FR,2019-05-08 01:00:00+00:00,FR04014,no2,19.6,µg/m³ +Paris,FR,2019-05-08 00:00:00+00:00,FR04014,no2,22.1,µg/m³ +Paris,FR,2019-05-07 23:00:00+00:00,FR04014,no2,34.0,µg/m³ +Paris,FR,2019-05-07 22:00:00+00:00,FR04014,no2,35.8,µg/m³ +Paris,FR,2019-05-07 21:00:00+00:00,FR04014,no2,33.9,µg/m³ +Paris,FR,2019-05-07 20:00:00+00:00,FR04014,no2,36.2,µg/m³ +Paris,FR,2019-05-07 19:00:00+00:00,FR04014,no2,26.8,µg/m³ +Paris,FR,2019-05-07 18:00:00+00:00,FR04014,no2,21.4,µg/m³ +Paris,FR,2019-05-07 17:00:00+00:00,FR04014,no2,22.3,µg/m³ +Paris,FR,2019-05-07 16:00:00+00:00,FR04014,no2,18.2,µg/m³ +Paris,FR,2019-05-07 15:00:00+00:00,FR04014,no2,11.7,µg/m³ +Paris,FR,2019-05-07 14:00:00+00:00,FR04014,no2,11.0,µg/m³ +Paris,FR,2019-05-07 13:00:00+00:00,FR04014,no2,13.2,µg/m³ +Paris,FR,2019-05-07 12:00:00+00:00,FR04014,no2,10.6,µg/m³ +Paris,FR,2019-05-07 11:00:00+00:00,FR04014,no2,13.0,µg/m³ +Paris,FR,2019-05-07 10:00:00+00:00,FR04014,no2,20.1,µg/m³ +Paris,FR,2019-05-07 09:00:00+00:00,FR04014,no2,34.5,µg/m³ +Paris,FR,2019-05-07 08:00:00+00:00,FR04014,no2,56.0,µg/m³ +Paris,FR,2019-05-07 07:00:00+00:00,FR04014,no2,67.9,µg/m³ +Paris,FR,2019-05-07 06:00:00+00:00,FR04014,no2,77.7,µg/m³ +Paris,FR,2019-05-07 05:00:00+00:00,FR04014,no2,72.4,µg/m³ +Paris,FR,2019-05-07 04:00:00+00:00,FR04014,no2,61.9,µg/m³ +Paris,FR,2019-05-07 03:00:00+00:00,FR04014,no2,50.4,µg/m³ +Paris,FR,2019-05-07 02:00:00+00:00,FR04014,no2,27.7,µg/m³ +Paris,FR,2019-05-07 01:00:00+00:00,FR04014,no2,25.0,µg/m³ +Antwerpen,BE,2019-06-17 08:00:00+00:00,BETR801,no2,41.0,µg/m³ +Antwerpen,BE,2019-06-17 07:00:00+00:00,BETR801,no2,45.0,µg/m³ +Antwerpen,BE,2019-06-17 06:00:00+00:00,BETR801,no2,43.5,µg/m³ +Antwerpen,BE,2019-06-17 05:00:00+00:00,BETR801,no2,42.5,µg/m³ +Antwerpen,BE,2019-06-17 04:00:00+00:00,BETR801,no2,39.5,µg/m³ +Antwerpen,BE,2019-06-17 03:00:00+00:00,BETR801,no2,36.0,µg/m³ +Antwerpen,BE,2019-06-17 02:00:00+00:00,BETR801,no2,35.5,µg/m³ +Antwerpen,BE,2019-06-17 01:00:00+00:00,BETR801,no2,42.0,µg/m³ +Antwerpen,BE,2019-06-16 01:00:00+00:00,BETR801,no2,42.5,µg/m³ +Antwerpen,BE,2019-06-15 01:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-06-14 09:00:00+00:00,BETR801,no2,36.5,µg/m³ +Antwerpen,BE,2019-06-13 01:00:00+00:00,BETR801,no2,28.5,µg/m³ +Antwerpen,BE,2019-06-12 01:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-06-11 01:00:00+00:00,BETR801,no2,7.5,µg/m³ +Antwerpen,BE,2019-06-10 01:00:00+00:00,BETR801,no2,18.5,µg/m³ +Antwerpen,BE,2019-06-09 01:00:00+00:00,BETR801,no2,10.0,µg/m³ +Antwerpen,BE,2019-06-05 01:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-06-01 01:00:00+00:00,BETR801,no2,52.5,µg/m³ +Antwerpen,BE,2019-05-31 01:00:00+00:00,BETR801,no2,9.0,µg/m³ +Antwerpen,BE,2019-05-30 01:00:00+00:00,BETR801,no2,7.5,µg/m³ +Antwerpen,BE,2019-05-29 01:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-05-28 01:00:00+00:00,BETR801,no2,11.0,µg/m³ +Antwerpen,BE,2019-05-27 01:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-05-26 01:00:00+00:00,BETR801,no2,53.0,µg/m³ +Antwerpen,BE,2019-05-25 01:00:00+00:00,BETR801,no2,29.0,µg/m³ +Antwerpen,BE,2019-05-24 01:00:00+00:00,BETR801,no2,74.5,µg/m³ +Antwerpen,BE,2019-05-23 01:00:00+00:00,BETR801,no2,60.5,µg/m³ +Antwerpen,BE,2019-05-22 01:00:00+00:00,BETR801,no2,20.5,µg/m³ +Antwerpen,BE,2019-05-21 01:00:00+00:00,BETR801,no2,15.5,µg/m³ +Antwerpen,BE,2019-05-20 15:00:00+00:00,BETR801,no2,25.5,µg/m³ +Antwerpen,BE,2019-05-20 14:00:00+00:00,BETR801,no2,24.5,µg/m³ +Antwerpen,BE,2019-05-20 13:00:00+00:00,BETR801,no2,32.0,µg/m³ +Antwerpen,BE,2019-05-20 12:00:00+00:00,BETR801,no2,34.5,µg/m³ +Antwerpen,BE,2019-05-20 11:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-05-20 10:00:00+00:00,BETR801,no2,25.0,µg/m³ +Antwerpen,BE,2019-05-20 09:00:00+00:00,BETR801,no2,30.5,µg/m³ +Antwerpen,BE,2019-05-20 08:00:00+00:00,BETR801,no2,40.0,µg/m³ +Antwerpen,BE,2019-05-20 07:00:00+00:00,BETR801,no2,38.0,µg/m³ +Antwerpen,BE,2019-05-20 06:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-20 05:00:00+00:00,BETR801,no2,20.0,µg/m³ +Antwerpen,BE,2019-05-20 04:00:00+00:00,BETR801,no2,14.0,µg/m³ +Antwerpen,BE,2019-05-20 03:00:00+00:00,BETR801,no2,9.0,µg/m³ +Antwerpen,BE,2019-05-20 02:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-05-20 01:00:00+00:00,BETR801,no2,17.0,µg/m³ +Antwerpen,BE,2019-05-20 00:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-19 23:00:00+00:00,BETR801,no2,16.5,µg/m³ +Antwerpen,BE,2019-05-19 22:00:00+00:00,BETR801,no2,18.5,µg/m³ +Antwerpen,BE,2019-05-19 21:00:00+00:00,BETR801,no2,12.5,µg/m³ +Antwerpen,BE,2019-05-19 20:00:00+00:00,BETR801,no2,15.0,µg/m³ +Antwerpen,BE,2019-05-19 19:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-19 18:00:00+00:00,BETR801,no2,15.5,µg/m³ +Antwerpen,BE,2019-05-19 17:00:00+00:00,BETR801,no2,18.5,µg/m³ +Antwerpen,BE,2019-05-19 16:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-05-19 15:00:00+00:00,BETR801,no2,33.0,µg/m³ +Antwerpen,BE,2019-05-19 14:00:00+00:00,BETR801,no2,23.0,µg/m³ +Antwerpen,BE,2019-05-19 13:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-19 12:00:00+00:00,BETR801,no2,16.0,µg/m³ +Antwerpen,BE,2019-05-19 11:00:00+00:00,BETR801,no2,17.0,µg/m³ +Antwerpen,BE,2019-05-19 10:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-05-19 09:00:00+00:00,BETR801,no2,16.0,µg/m³ +Antwerpen,BE,2019-05-19 08:00:00+00:00,BETR801,no2,23.5,µg/m³ +Antwerpen,BE,2019-05-19 07:00:00+00:00,BETR801,no2,30.0,µg/m³ +Antwerpen,BE,2019-05-19 06:00:00+00:00,BETR801,no2,30.5,µg/m³ +Antwerpen,BE,2019-05-19 05:00:00+00:00,BETR801,no2,26.0,µg/m³ +Antwerpen,BE,2019-05-19 04:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-05-19 03:00:00+00:00,BETR801,no2,19.0,µg/m³ +Antwerpen,BE,2019-05-19 02:00:00+00:00,BETR801,no2,19.0,µg/m³ +Antwerpen,BE,2019-05-19 01:00:00+00:00,BETR801,no2,22.5,µg/m³ +Antwerpen,BE,2019-05-19 00:00:00+00:00,BETR801,no2,23.5,µg/m³ +Antwerpen,BE,2019-05-18 23:00:00+00:00,BETR801,no2,29.5,µg/m³ +Antwerpen,BE,2019-05-18 22:00:00+00:00,BETR801,no2,34.5,µg/m³ +Antwerpen,BE,2019-05-18 21:00:00+00:00,BETR801,no2,39.0,µg/m³ +Antwerpen,BE,2019-05-18 20:00:00+00:00,BETR801,no2,40.0,µg/m³ +Antwerpen,BE,2019-05-18 19:00:00+00:00,BETR801,no2,35.5,µg/m³ +Antwerpen,BE,2019-05-18 18:00:00+00:00,BETR801,no2,35.5,µg/m³ +Antwerpen,BE,2019-05-18 01:00:00+00:00,BETR801,no2,41.5,µg/m³ +Antwerpen,BE,2019-05-16 01:00:00+00:00,BETR801,no2,28.0,µg/m³ +Antwerpen,BE,2019-05-15 02:00:00+00:00,BETR801,no2,22.5,µg/m³ +Antwerpen,BE,2019-05-15 01:00:00+00:00,BETR801,no2,25.5,µg/m³ +Antwerpen,BE,2019-05-14 02:00:00+00:00,BETR801,no2,11.5,µg/m³ +Antwerpen,BE,2019-05-14 01:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-13 02:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-13 01:00:00+00:00,BETR801,no2,14.5,µg/m³ +Antwerpen,BE,2019-05-12 02:00:00+00:00,BETR801,no2,20.0,µg/m³ +Antwerpen,BE,2019-05-12 01:00:00+00:00,BETR801,no2,17.5,µg/m³ +Antwerpen,BE,2019-05-11 02:00:00+00:00,BETR801,no2,21.0,µg/m³ +Antwerpen,BE,2019-05-11 01:00:00+00:00,BETR801,no2,26.5,µg/m³ +Antwerpen,BE,2019-05-10 02:00:00+00:00,BETR801,no2,11.5,µg/m³ +Antwerpen,BE,2019-05-10 01:00:00+00:00,BETR801,no2,10.5,µg/m³ +Antwerpen,BE,2019-05-09 02:00:00+00:00,BETR801,no2,20.5,µg/m³ +Antwerpen,BE,2019-05-09 01:00:00+00:00,BETR801,no2,20.0,µg/m³ +Antwerpen,BE,2019-05-08 02:00:00+00:00,BETR801,no2,20.5,µg/m³ +Antwerpen,BE,2019-05-08 01:00:00+00:00,BETR801,no2,23.0,µg/m³ +Antwerpen,BE,2019-05-07 02:00:00+00:00,BETR801,no2,45.0,µg/m³ +Antwerpen,BE,2019-05-07 01:00:00+00:00,BETR801,no2,50.5,µg/m³ +London,GB,2019-06-17 11:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 10:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 09:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 08:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-17 07:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-17 06:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-17 05:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 04:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-17 03:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-17 02:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-17 01:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-17 00:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-16 23:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-16 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-16 20:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-16 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-16 18:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-16 17:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-16 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-16 15:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-16 14:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-16 13:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-16 12:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-16 11:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-16 10:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-16 09:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-16 08:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-16 07:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-16 06:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-16 05:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-16 04:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-16 03:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-16 02:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-16 01:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-16 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-15 23:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-15 22:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-15 21:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-15 20:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-15 19:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-15 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-15 17:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-15 16:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-15 15:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-15 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-15 13:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-15 12:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-15 11:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-15 10:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-15 09:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-15 08:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-15 07:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-15 06:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-15 05:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-15 04:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-15 00:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-14 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 19:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 18:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-14 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-14 16:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-14 15:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-14 14:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-14 13:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-14 12:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-14 11:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-14 10:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-14 09:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-14 08:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-14 07:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-14 06:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-14 05:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-14 04:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-14 03:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-14 02:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-14 00:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-13 23:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 22:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 21:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 20:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-13 19:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-13 18:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-13 17:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 16:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-13 15:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-13 14:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-13 13:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 12:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 11:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 10:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-13 09:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 08:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 07:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-13 06:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-13 05:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 04:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-13 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-13 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-13 00:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-12 23:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-12 21:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-12 20:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-06-12 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-12 18:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-12 17:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-12 16:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-12 15:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-06-12 14:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-12 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-12 12:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-12 11:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-12 10:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-12 09:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-12 08:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-12 07:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-12 06:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-12 05:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-12 04:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-12 03:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-12 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-11 23:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-11 22:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-11 21:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 20:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 19:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-06-11 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-11 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-06-11 16:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-11 15:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-11 14:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-11 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-11 12:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-11 11:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 10:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-11 09:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-11 08:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-11 07:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-11 06:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-11 05:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-11 04:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-11 03:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-11 02:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-11 01:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-11 00:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-10 23:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-10 22:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-10 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-10 19:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-10 18:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-10 17:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-10 16:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-10 15:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-06-10 14:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-06-10 13:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-06-10 12:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-10 11:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-06-10 10:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-06-10 09:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-10 08:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-10 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-10 06:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 05:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 04:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-10 03:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-10 02:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-10 01:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-10 00:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-09 23:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-09 21:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-09 20:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-09 19:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-09 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-09 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-06-09 16:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-09 15:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-09 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-09 13:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-09 12:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-09 11:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-09 10:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-09 09:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-09 08:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-09 07:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-09 06:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-09 05:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-09 04:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-09 03:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-09 02:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-09 01:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-09 00:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-08 23:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-08 21:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-08 20:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-08 19:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-08 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-08 17:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-08 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-08 15:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-08 14:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-08 13:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-08 12:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-08 11:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-08 10:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 09:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-08 08:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-08 07:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 06:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-08 05:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 04:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-08 03:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-08 02:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-08 00:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-07 23:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-07 21:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-07 20:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-07 19:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-07 18:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-07 16:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-07 15:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-07 14:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-07 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-07 12:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 11:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-07 10:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 09:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-07 08:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-07 07:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 06:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-06-07 05:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 04:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 03:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 02:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-07 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-07 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-06 23:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-06 22:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-06 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 20:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-06 19:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 18:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-06 16:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-06 15:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-06 14:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-06 13:00:00+00:00,London Westminster,no2,10.0,µg/m³ +London,GB,2019-06-06 12:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-06 11:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-06 10:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-06 09:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-06 08:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-06 07:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-06 06:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-06 05:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-06 04:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-06 03:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-06 02:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-06 00:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-05 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-05 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-05 21:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 20:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 19:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 18:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 17:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 16:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-05 15:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-05 14:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-05 13:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-05 12:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-05 11:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-05 10:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-05 09:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-05 08:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-06-05 07:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-06-05 06:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-05 05:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-05 04:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-05 03:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-05 02:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-05 01:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-05 00:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-06-04 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-04 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-04 21:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-04 20:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-04 19:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-04 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-06-04 17:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-06-04 16:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-06-04 15:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-06-04 14:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-04 13:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-06-04 12:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-04 11:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-04 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-04 09:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-04 08:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-04 07:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-06-04 06:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-04 05:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-04 04:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-04 03:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-04 02:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-04 01:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-04 00:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-03 23:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-03 22:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-03 21:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-03 20:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-06-03 19:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-06-03 18:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-03 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-06-03 16:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-03 15:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-03 14:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-06-03 13:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-06-03 12:00:00+00:00,London Westminster,no2,14.0,µg/m³ +London,GB,2019-06-03 11:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-03 10:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-03 09:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-03 08:00:00+00:00,London Westminster,no2,7.0,µg/m³ +London,GB,2019-06-03 07:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-06-03 06:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-06-03 05:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-03 04:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-06-03 03:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-03 02:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-03 01:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-03 00:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-06-02 23:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-02 22:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-06-02 21:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-02 20:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 19:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-02 18:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-02 17:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 15:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-02 14:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-02 13:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-06-02 12:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-02 11:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-02 10:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-06-02 09:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 08:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-06-02 07:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 06:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-02 05:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-06-02 04:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-06-02 03:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-02 02:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-06-02 01:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-06-02 00:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-06-01 23:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-06-01 22:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-06-01 21:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-06-01 20:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-06-01 19:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-06-01 18:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-01 17:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-06-01 16:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-06-01 15:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-01 14:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-06-01 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-06-01 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-06-01 11:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-06-01 10:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-06-01 09:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-06-01 08:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-06-01 07:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-06-01 06:00:00+00:00,London Westminster,no2,4.0,µg/m³ +London,GB,2019-06-01 05:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-01 04:00:00+00:00,London Westminster,no2,11.0,µg/m³ +London,GB,2019-06-01 03:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-01 02:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-06-01 01:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-06-01 00:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-31 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-31 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-31 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-31 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-31 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 16:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-31 15:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-31 14:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-31 13:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-31 12:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-31 11:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-31 10:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-31 09:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-31 08:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-05-31 07:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-31 06:00:00+00:00,London Westminster,no2,8.0,µg/m³ +London,GB,2019-05-31 05:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-31 04:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-31 03:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-05-31 02:00:00+00:00,London Westminster,no2,12.0,µg/m³ +London,GB,2019-05-31 01:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-31 00:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-30 23:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-30 22:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-30 21:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-30 20:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-30 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-30 18:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-30 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-30 16:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-30 15:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-30 14:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-30 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-30 12:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-30 11:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-05-30 10:00:00+00:00,London Westminster,no2,9.0,µg/m³ +London,GB,2019-05-30 09:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-30 08:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-05-30 07:00:00+00:00,London Westminster,no2,2.0,µg/m³ +London,GB,2019-05-30 06:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 05:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 04:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 03:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 02:00:00+00:00,London Westminster,no2,0.0,µg/m³ +London,GB,2019-05-30 01:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-05-30 00:00:00+00:00,London Westminster,no2,1.0,µg/m³ +London,GB,2019-05-29 23:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 22:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 21:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-05-29 20:00:00+00:00,London Westminster,no2,6.0,µg/m³ +London,GB,2019-05-29 19:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 18:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 17:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 16:00:00+00:00,London Westminster,no2,3.0,µg/m³ +London,GB,2019-05-29 15:00:00+00:00,London Westminster,no2,5.0,µg/m³ +London,GB,2019-05-29 14:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-29 13:00:00+00:00,London Westminster,no2,13.0,µg/m³ +London,GB,2019-05-29 12:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-29 11:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-29 10:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-29 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-29 08:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-29 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-29 06:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-29 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-29 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-29 03:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-29 02:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-29 01:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-29 00:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-28 23:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-28 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-28 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 19:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 17:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-28 16:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-28 15:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-28 14:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-28 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-28 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-28 11:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-28 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-28 09:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-28 08:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-28 07:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-28 06:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-28 05:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-28 04:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-28 03:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-28 02:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-28 01:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-28 00:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-27 23:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 22:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 21:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-27 20:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-27 19:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 17:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 16:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 15:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-27 14:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-27 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-27 12:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-27 11:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-27 10:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-27 09:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 08:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 07:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 06:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-27 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-27 03:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-27 02:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-27 01:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-27 00:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 21:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-26 20:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-26 19:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 18:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-26 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 16:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-26 15:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 14:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-26 13:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-26 12:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-26 11:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-26 10:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-26 09:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-26 08:00:00+00:00,London Westminster,no2,15.0,µg/m³ +London,GB,2019-05-26 07:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-26 06:00:00+00:00,London Westminster,no2,17.0,µg/m³ +London,GB,2019-05-26 05:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-26 04:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-26 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-26 01:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-26 00:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-25 23:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-25 22:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-25 21:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-25 20:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-25 19:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-25 18:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-25 17:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-25 16:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-25 15:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-25 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-25 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-25 12:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-25 11:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-25 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-25 09:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-25 08:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-25 07:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-25 06:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-25 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-25 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-25 03:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-25 02:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-25 01:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-25 00:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-24 23:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-24 22:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-24 21:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-24 20:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-24 19:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-24 18:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-24 17:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-24 16:00:00+00:00,London Westminster,no2,43.0,µg/m³ +London,GB,2019-05-24 15:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-24 14:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-24 13:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-24 12:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-24 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-24 10:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-24 09:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-24 08:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-24 07:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-24 06:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-24 05:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-24 04:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-24 03:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-24 02:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-24 00:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-23 23:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-23 22:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-23 21:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-23 20:00:00+00:00,London Westminster,no2,45.0,µg/m³ +London,GB,2019-05-23 19:00:00+00:00,London Westminster,no2,51.0,µg/m³ +London,GB,2019-05-23 18:00:00+00:00,London Westminster,no2,54.0,µg/m³ +London,GB,2019-05-23 17:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-05-23 16:00:00+00:00,London Westminster,no2,53.0,µg/m³ +London,GB,2019-05-23 15:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-23 14:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-23 13:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-23 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-23 11:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-23 10:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-23 09:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-23 08:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-23 07:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-23 06:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-23 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-23 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-23 03:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-23 02:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-23 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-23 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-22 23:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-22 22:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-22 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-22 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-22 18:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-22 17:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-22 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-22 15:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-22 14:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-22 13:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-22 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-22 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-22 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 09:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 08:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-22 07:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-22 06:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-22 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-22 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-22 03:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-22 02:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-22 01:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-22 00:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-21 23:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 22:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 21:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 20:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-21 19:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-21 18:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-21 17:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-21 16:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-21 15:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-21 14:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-21 13:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-21 12:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 10:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-21 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-21 08:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-21 07:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-21 06:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-21 05:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-21 04:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-21 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-21 01:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-21 00:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-20 23:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-20 22:00:00+00:00,London Westminster,no2,47.0,µg/m³ +London,GB,2019-05-20 21:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-20 20:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 19:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 18:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-20 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-20 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 15:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 14:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 13:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 08:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-20 07:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-20 06:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-20 05:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-20 04:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-20 03:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 02:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-20 01:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-20 00:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-19 23:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 22:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 21:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 20:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-19 19:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-19 18:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-19 17:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-19 16:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-19 15:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-19 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-19 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-19 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-19 11:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-19 10:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-19 09:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-19 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-19 07:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-19 06:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-19 05:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 04:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 03:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 02:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 01:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-19 00:00:00+00:00,London Westminster,no2,49.0,µg/m³ +London,GB,2019-05-18 23:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-18 22:00:00+00:00,London Westminster,no2,46.0,µg/m³ +London,GB,2019-05-18 21:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-18 20:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-18 19:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-18 18:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-18 17:00:00+00:00,London Westminster,no2,42.0,µg/m³ +London,GB,2019-05-18 16:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-18 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-18 14:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-18 13:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 12:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-18 11:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-18 10:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-18 09:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-18 08:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-18 07:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 06:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-18 05:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 04:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-18 03:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-18 02:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-18 01:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-18 00:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-17 23:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-17 22:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-17 21:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-17 20:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-17 19:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-17 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-17 17:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-17 16:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 15:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-17 14:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-17 13:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-17 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 11:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 10:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-17 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-17 08:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-17 07:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-17 06:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-17 05:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 04:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 03:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 02:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-17 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-17 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-16 23:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 22:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 20:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 19:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-16 18:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-16 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-16 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-16 15:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 14:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-16 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-16 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-16 11:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-16 09:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-16 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-16 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 05:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 04:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-16 03:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-16 02:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-16 01:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-16 00:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 23:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 22:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 21:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-15 20:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-15 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 18:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-15 17:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 16:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-15 15:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-15 14:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-15 13:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-15 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-15 11:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-15 10:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-15 09:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-15 08:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-15 07:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-15 05:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-15 04:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-15 03:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-15 02:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-15 00:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-14 23:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 22:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 21:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 20:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-14 19:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-14 18:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 17:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-14 16:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-14 15:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-14 14:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 13:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 12:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-14 11:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-14 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-14 09:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 08:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-14 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-14 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-14 05:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 04:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-14 03:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 02:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-14 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-14 00:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-13 23:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 22:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 21:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 20:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 19:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 18:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-13 17:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-13 16:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-13 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-13 14:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-13 13:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-13 12:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-13 11:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-13 10:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-13 09:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-13 08:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-13 07:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-13 06:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-13 05:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-13 04:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-13 03:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-13 02:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-13 01:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-13 00:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 23:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 22:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 21:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-12 20:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 19:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 18:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 17:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-12 16:00:00+00:00,London Westminster,no2,23.0,µg/m³ +London,GB,2019-05-12 15:00:00+00:00,London Westminster,no2,22.0,µg/m³ +London,GB,2019-05-12 14:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-12 13:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-12 12:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-12 11:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-12 10:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-12 09:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-12 08:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-12 07:00:00+00:00,London Westminster,no2,44.0,µg/m³ +London,GB,2019-05-12 06:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-12 05:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-12 04:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-12 03:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-12 02:00:00+00:00,London Westminster,no2,38.0,µg/m³ +London,GB,2019-05-12 01:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-12 00:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-11 23:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-11 22:00:00+00:00,London Westminster,no2,37.0,µg/m³ +London,GB,2019-05-11 21:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-11 20:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-11 19:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-11 18:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-11 17:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-11 16:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-11 15:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-11 09:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-11 08:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-11 07:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-11 06:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-11 05:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-11 04:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-11 03:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-11 02:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-11 01:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-11 00:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-10 23:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-10 22:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-10 21:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 19:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 18:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-10 17:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-10 16:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-10 15:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-10 14:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-10 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-10 12:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-10 11:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-10 10:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-10 09:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-10 08:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-10 07:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-10 06:00:00+00:00,London Westminster,no2,39.0,µg/m³ +London,GB,2019-05-10 05:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-10 04:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-10 03:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-10 02:00:00+00:00,London Westminster,no2,41.0,µg/m³ +London,GB,2019-05-10 01:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-05-10 00:00:00+00:00,London Westminster,no2,52.0,µg/m³ +London,GB,2019-05-09 23:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-05-09 22:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-05-09 21:00:00+00:00,London Westminster,no2,65.0,µg/m³ +London,GB,2019-05-09 20:00:00+00:00,London Westminster,no2,59.0,µg/m³ +London,GB,2019-05-09 19:00:00+00:00,London Westminster,no2,62.0,µg/m³ +London,GB,2019-05-09 18:00:00+00:00,London Westminster,no2,58.0,µg/m³ +London,GB,2019-05-09 17:00:00+00:00,London Westminster,no2,60.0,µg/m³ +London,GB,2019-05-09 16:00:00+00:00,London Westminster,no2,67.0,µg/m³ +London,GB,2019-05-09 15:00:00+00:00,London Westminster,no2,97.0,µg/m³ +London,GB,2019-05-09 14:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-09 13:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-09 12:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-09 11:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-09 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-09 09:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-09 08:00:00+00:00,London Westminster,no2,35.0,µg/m³ +London,GB,2019-05-09 07:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 06:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 05:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 04:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-09 03:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-09 02:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-09 00:00:00+00:00,London Westminster,no2,30.0,µg/m³ +London,GB,2019-05-08 23:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-08 21:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-08 20:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-08 19:00:00+00:00,London Westminster,no2,25.0,µg/m³ +London,GB,2019-05-08 18:00:00+00:00,London Westminster,no2,40.0,µg/m³ +London,GB,2019-05-08 17:00:00+00:00,London Westminster,no2,31.0,µg/m³ +London,GB,2019-05-08 16:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-08 15:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-08 14:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-08 13:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-08 12:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-08 11:00:00+00:00,London Westminster,no2,27.0,µg/m³ +London,GB,2019-05-08 10:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-08 09:00:00+00:00,London Westminster,no2,33.0,µg/m³ +London,GB,2019-05-08 08:00:00+00:00,London Westminster,no2,36.0,µg/m³ +London,GB,2019-05-08 07:00:00+00:00,London Westminster,no2,34.0,µg/m³ +London,GB,2019-05-08 06:00:00+00:00,London Westminster,no2,29.0,µg/m³ +London,GB,2019-05-08 05:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-08 04:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-08 03:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-08 02:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-08 01:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-08 00:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-07 23:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-07 21:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-07 20:00:00+00:00,London Westminster,no2,24.0,µg/m³ +London,GB,2019-05-07 19:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 18:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 17:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-07 16:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 15:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 14:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-07 13:00:00+00:00,London Westminster,no2,20.0,µg/m³ +London,GB,2019-05-07 12:00:00+00:00,London Westminster,no2,18.0,µg/m³ +London,GB,2019-05-07 11:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-07 10:00:00+00:00,London Westminster,no2,21.0,µg/m³ +London,GB,2019-05-07 09:00:00+00:00,London Westminster,no2,28.0,µg/m³ +London,GB,2019-05-07 08:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-07 07:00:00+00:00,London Westminster,no2,32.0,µg/m³ +London,GB,2019-05-07 06:00:00+00:00,London Westminster,no2,26.0,µg/m³ +London,GB,2019-05-07 04:00:00+00:00,London Westminster,no2,16.0,µg/m³ +London,GB,2019-05-07 03:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-07 02:00:00+00:00,London Westminster,no2,19.0,µg/m³ +London,GB,2019-05-07 01:00:00+00:00,London Westminster,no2,23.0,µg/m³ diff --git a/doc/data/air_quality_parameters.csv b/doc/data/air_quality_parameters.csv new file mode 100644 index 0000000000000..915f6300e43b8 --- /dev/null +++ b/doc/data/air_quality_parameters.csv @@ -0,0 +1,8 @@ +id,description,name +bc,Black Carbon,BC +co,Carbon Monoxide,CO +no2,Nitrogen Dioxide,NO2 +o3,Ozone,O3 +pm10,Particulate matter less than 10 micrometers in diameter,PM10 +pm25,Particulate matter less than 2.5 micrometers in diameter,PM2.5 +so2,Sulfur Dioxide,SO2 diff --git a/doc/data/air_quality_pm25_long.csv b/doc/data/air_quality_pm25_long.csv new file mode 100644 index 0000000000000..f74053c249339 --- /dev/null +++ b/doc/data/air_quality_pm25_long.csv @@ -0,0 +1,1111 @@ +city,country,date.utc,location,parameter,value,unit +Antwerpen,BE,2019-06-18 06:00:00+00:00,BETR801,pm25,18.0,µg/m³ +Antwerpen,BE,2019-06-17 08:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-06-17 07:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-06-17 06:00:00+00:00,BETR801,pm25,16.0,µg/m³ +Antwerpen,BE,2019-06-17 05:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-06-17 04:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-06-17 03:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-06-17 02:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-06-17 01:00:00+00:00,BETR801,pm25,8.0,µg/m³ +Antwerpen,BE,2019-06-16 01:00:00+00:00,BETR801,pm25,15.0,µg/m³ +Antwerpen,BE,2019-06-15 01:00:00+00:00,BETR801,pm25,11.0,µg/m³ +Antwerpen,BE,2019-06-14 09:00:00+00:00,BETR801,pm25,12.0,µg/m³ +Antwerpen,BE,2019-06-13 01:00:00+00:00,BETR801,pm25,3.0,µg/m³ +Antwerpen,BE,2019-06-12 01:00:00+00:00,BETR801,pm25,16.0,µg/m³ +Antwerpen,BE,2019-06-11 01:00:00+00:00,BETR801,pm25,3.5,µg/m³ +Antwerpen,BE,2019-06-10 01:00:00+00:00,BETR801,pm25,8.5,µg/m³ +Antwerpen,BE,2019-06-09 01:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-06-08 01:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-06-06 01:00:00+00:00,BETR801,pm25,6.5,µg/m³ +Antwerpen,BE,2019-06-05 01:00:00+00:00,BETR801,pm25,11.0,µg/m³ +Antwerpen,BE,2019-06-04 01:00:00+00:00,BETR801,pm25,10.5,µg/m³ +Antwerpen,BE,2019-06-03 01:00:00+00:00,BETR801,pm25,12.5,µg/m³ +Antwerpen,BE,2019-06-02 01:00:00+00:00,BETR801,pm25,19.0,µg/m³ +Antwerpen,BE,2019-06-01 01:00:00+00:00,BETR801,pm25,9.0,µg/m³ +Antwerpen,BE,2019-05-31 01:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-05-30 01:00:00+00:00,BETR801,pm25,5.0,µg/m³ +Antwerpen,BE,2019-05-29 01:00:00+00:00,BETR801,pm25,5.5,µg/m³ +Antwerpen,BE,2019-05-28 01:00:00+00:00,BETR801,pm25,7.0,µg/m³ +Antwerpen,BE,2019-05-27 01:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-05-26 01:00:00+00:00,BETR801,pm25,26.5,µg/m³ +Antwerpen,BE,2019-05-25 01:00:00+00:00,BETR801,pm25,10.0,µg/m³ +Antwerpen,BE,2019-05-24 01:00:00+00:00,BETR801,pm25,13.0,µg/m³ +Antwerpen,BE,2019-05-23 01:00:00+00:00,BETR801,pm25,7.5,µg/m³ +Antwerpen,BE,2019-05-22 01:00:00+00:00,BETR801,pm25,15.5,µg/m³ +Antwerpen,BE,2019-05-21 01:00:00+00:00,BETR801,pm25,20.5,µg/m³ +Antwerpen,BE,2019-05-20 17:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-05-20 16:00:00+00:00,BETR801,pm25,17.0,µg/m³ +Antwerpen,BE,2019-05-20 15:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-05-20 14:00:00+00:00,BETR801,pm25,14.5,µg/m³ +Antwerpen,BE,2019-05-20 13:00:00+00:00,BETR801,pm25,17.0,µg/m³ +Antwerpen,BE,2019-05-20 12:00:00+00:00,BETR801,pm25,17.5,µg/m³ +Antwerpen,BE,2019-05-20 11:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-20 10:00:00+00:00,BETR801,pm25,10.5,µg/m³ +Antwerpen,BE,2019-05-20 09:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-20 08:00:00+00:00,BETR801,pm25,19.5,µg/m³ +Antwerpen,BE,2019-05-20 07:00:00+00:00,BETR801,pm25,23.5,µg/m³ +Antwerpen,BE,2019-05-20 06:00:00+00:00,BETR801,pm25,22.0,µg/m³ +Antwerpen,BE,2019-05-20 05:00:00+00:00,BETR801,pm25,25.0,µg/m³ +Antwerpen,BE,2019-05-20 04:00:00+00:00,BETR801,pm25,24.5,µg/m³ +Antwerpen,BE,2019-05-20 03:00:00+00:00,BETR801,pm25,15.0,µg/m³ +Antwerpen,BE,2019-05-20 02:00:00+00:00,BETR801,pm25,18.5,µg/m³ +Antwerpen,BE,2019-05-20 01:00:00+00:00,BETR801,pm25,28.0,µg/m³ +Antwerpen,BE,2019-05-19 21:00:00+00:00,BETR801,pm25,35.5,µg/m³ +Antwerpen,BE,2019-05-19 20:00:00+00:00,BETR801,pm25,40.0,µg/m³ +Antwerpen,BE,2019-05-19 19:00:00+00:00,BETR801,pm25,43.5,µg/m³ +Antwerpen,BE,2019-05-19 18:00:00+00:00,BETR801,pm25,35.0,µg/m³ +Antwerpen,BE,2019-05-19 17:00:00+00:00,BETR801,pm25,34.0,µg/m³ +Antwerpen,BE,2019-05-19 16:00:00+00:00,BETR801,pm25,36.5,µg/m³ +Antwerpen,BE,2019-05-19 15:00:00+00:00,BETR801,pm25,44.0,µg/m³ +Antwerpen,BE,2019-05-19 14:00:00+00:00,BETR801,pm25,43.5,µg/m³ +Antwerpen,BE,2019-05-19 13:00:00+00:00,BETR801,pm25,46.0,µg/m³ +Antwerpen,BE,2019-05-19 12:00:00+00:00,BETR801,pm25,43.0,µg/m³ +Antwerpen,BE,2019-05-19 11:00:00+00:00,BETR801,pm25,41.0,µg/m³ +Antwerpen,BE,2019-05-19 10:00:00+00:00,BETR801,pm25,41.5,µg/m³ +Antwerpen,BE,2019-05-19 09:00:00+00:00,BETR801,pm25,42.5,µg/m³ +Antwerpen,BE,2019-05-19 08:00:00+00:00,BETR801,pm25,51.5,µg/m³ +Antwerpen,BE,2019-05-19 07:00:00+00:00,BETR801,pm25,56.0,µg/m³ +Antwerpen,BE,2019-05-19 06:00:00+00:00,BETR801,pm25,58.5,µg/m³ +Antwerpen,BE,2019-05-19 05:00:00+00:00,BETR801,pm25,60.0,µg/m³ +Antwerpen,BE,2019-05-19 04:00:00+00:00,BETR801,pm25,56.5,µg/m³ +Antwerpen,BE,2019-05-19 03:00:00+00:00,BETR801,pm25,52.5,µg/m³ +Antwerpen,BE,2019-05-19 02:00:00+00:00,BETR801,pm25,51.5,µg/m³ +Antwerpen,BE,2019-05-19 01:00:00+00:00,BETR801,pm25,52.0,µg/m³ +Antwerpen,BE,2019-05-19 00:00:00+00:00,BETR801,pm25,49.5,µg/m³ +Antwerpen,BE,2019-05-18 23:00:00+00:00,BETR801,pm25,45.5,µg/m³ +Antwerpen,BE,2019-05-18 22:00:00+00:00,BETR801,pm25,42.0,µg/m³ +Antwerpen,BE,2019-05-18 21:00:00+00:00,BETR801,pm25,40.5,µg/m³ +Antwerpen,BE,2019-05-18 20:00:00+00:00,BETR801,pm25,41.0,µg/m³ +Antwerpen,BE,2019-05-18 19:00:00+00:00,BETR801,pm25,36.5,µg/m³ +Antwerpen,BE,2019-05-18 18:00:00+00:00,BETR801,pm25,37.0,µg/m³ +Antwerpen,BE,2019-05-18 01:00:00+00:00,BETR801,pm25,24.0,µg/m³ +Antwerpen,BE,2019-05-17 01:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-16 01:00:00+00:00,BETR801,pm25,11.0,µg/m³ +Antwerpen,BE,2019-05-15 02:00:00+00:00,BETR801,pm25,12.5,µg/m³ +Antwerpen,BE,2019-05-15 01:00:00+00:00,BETR801,pm25,13.0,µg/m³ +Antwerpen,BE,2019-05-14 02:00:00+00:00,BETR801,pm25,4.0,µg/m³ +Antwerpen,BE,2019-05-14 01:00:00+00:00,BETR801,pm25,4.0,µg/m³ +Antwerpen,BE,2019-05-13 02:00:00+00:00,BETR801,pm25,5.5,µg/m³ +Antwerpen,BE,2019-05-13 01:00:00+00:00,BETR801,pm25,5.0,µg/m³ +Antwerpen,BE,2019-05-12 02:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-05-12 01:00:00+00:00,BETR801,pm25,6.0,µg/m³ +Antwerpen,BE,2019-05-11 02:00:00+00:00,BETR801,pm25,19.5,µg/m³ +Antwerpen,BE,2019-05-11 01:00:00+00:00,BETR801,pm25,17.0,µg/m³ +Antwerpen,BE,2019-05-10 02:00:00+00:00,BETR801,pm25,13.5,µg/m³ +Antwerpen,BE,2019-05-10 01:00:00+00:00,BETR801,pm25,11.5,µg/m³ +Antwerpen,BE,2019-05-09 02:00:00+00:00,BETR801,pm25,3.5,µg/m³ +Antwerpen,BE,2019-05-09 01:00:00+00:00,BETR801,pm25,4.5,µg/m³ +Antwerpen,BE,2019-05-08 02:00:00+00:00,BETR801,pm25,14.0,µg/m³ +Antwerpen,BE,2019-05-08 01:00:00+00:00,BETR801,pm25,14.5,µg/m³ +Antwerpen,BE,2019-05-07 02:00:00+00:00,BETR801,pm25,14.0,µg/m³ +Antwerpen,BE,2019-05-07 01:00:00+00:00,BETR801,pm25,12.5,µg/m³ +London,GB,2019-06-21 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-20 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-20 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-20 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-20 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-19 13:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-19 12:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-19 11:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-19 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-06-18 23:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-06-18 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-06-18 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-18 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-18 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-17 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-17 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-16 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-16 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-15 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-15 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-14 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-14 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-14 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-13 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-13 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-13 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-13 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-13 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-12 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-12 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-11 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-11 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-10 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-10 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-09 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-09 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-08 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-08 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-08 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-08 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-08 03:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-08 02:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-08 00:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 23:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 21:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 20:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 19:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-06-07 18:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 17:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 16:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 15:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 14:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 13:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-06-07 12:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 11:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 10:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 09:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 08:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-06-07 07:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-06-07 06:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-06-07 05:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-06-07 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-06-07 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-07 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-07 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-07 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-06 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-06 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-06 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-06 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-05 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-05 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-05 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-05 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-05 01:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-05 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 23:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-06-04 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-04 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-04 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-03 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-03 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-06-02 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-02 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-06-01 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-06-01 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-06-01 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 22:00:00+00:00,London Westminster,pm25,5.0,µg/m³ +London,GB,2019-05-31 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-31 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-31 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-30 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-30 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-29 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-29 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-28 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-28 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-27 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-27 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 20:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 19:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 18:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 17:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 16:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 15:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 14:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 13:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 12:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 11:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 07:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 06:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 05:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 04:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 03:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 02:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 01:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-26 00:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 23:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 22:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 21:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-25 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-25 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-25 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-25 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-25 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-25 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-25 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-24 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-24 22:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-24 21:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 20:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 19:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 18:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-24 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-24 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 06:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-23 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 02:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-23 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 10:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 09:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 08:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-22 05:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-22 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-21 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-21 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-21 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-21 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-21 07:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-21 06:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-21 05:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-21 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-21 03:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-21 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-21 01:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-21 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 22:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 21:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 20:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 19:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 18:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-20 17:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-20 16:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-20 15:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 14:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 13:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 12:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 11:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 10:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-20 09:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-20 08:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-20 07:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-20 06:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-20 05:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-20 04:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 03:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 02:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 01:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-20 00:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 23:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 22:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 21:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 20:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 19:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 18:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 17:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 16:00:00+00:00,London Westminster,pm25,20.0,µg/m³ +London,GB,2019-05-19 15:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 14:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 13:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 12:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 11:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 10:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 09:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 08:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 07:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 06:00:00+00:00,London Westminster,pm25,19.0,µg/m³ +London,GB,2019-05-19 05:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 04:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 03:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 02:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 01:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-19 00:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 23:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 22:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 21:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 20:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 19:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 18:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 17:00:00+00:00,London Westminster,pm25,18.0,µg/m³ +London,GB,2019-05-18 16:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 15:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 14:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 13:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 12:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 11:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 10:00:00+00:00,London Westminster,pm25,17.0,µg/m³ +London,GB,2019-05-18 09:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-18 08:00:00+00:00,London Westminster,pm25,16.0,µg/m³ +London,GB,2019-05-18 07:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-18 06:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-18 05:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-18 04:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-18 03:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-18 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-18 01:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-18 00:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-17 23:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-17 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 21:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 20:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 19:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 13:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 12:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 11:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-17 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 06:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 04:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 03:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-17 01:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-17 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-16 23:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-16 22:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-16 21:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-16 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-16 16:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 15:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 14:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 13:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 12:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 11:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 10:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 09:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 08:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 07:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 06:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 05:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 04:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 03:00:00+00:00,London Westminster,pm25,15.0,µg/m³ +London,GB,2019-05-16 02:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 01:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-16 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-15 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-15 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-15 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-15 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-15 19:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 18:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 17:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 16:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-15 15:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 14:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 13:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 12:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-15 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-15 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-15 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-15 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 03:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 02:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-15 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-14 23:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-14 22:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 21:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 20:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 19:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 18:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 17:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 16:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 15:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 14:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 13:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 12:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 11:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 10:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 09:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 08:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 07:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 04:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 03:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-14 01:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-14 00:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 23:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 22:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 21:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 20:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 19:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 18:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 17:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 16:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 15:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 14:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 13:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 12:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 11:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 10:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-13 09:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-13 08:00:00+00:00,London Westminster,pm25,6.0,µg/m³ +London,GB,2019-05-13 07:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 06:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 05:00:00+00:00,London Westminster,pm25,7.0,µg/m³ +London,GB,2019-05-13 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-13 00:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-12 23:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-12 22:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 21:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 20:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 19:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 18:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 14:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-12 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-12 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-12 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-12 10:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 09:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 08:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 07:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-12 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-12 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-12 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-12 03:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-12 02:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-12 01:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-12 00:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-11 23:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-11 22:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 21:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 20:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 19:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 18:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 17:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 16:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 15:00:00+00:00,London Westminster,pm25,14.0,µg/m³ +London,GB,2019-05-11 09:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 08:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 07:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-11 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-11 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-11 04:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-11 03:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-11 02:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-11 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-11 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-10 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-10 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 21:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 20:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 19:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 18:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 17:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 16:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 15:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 14:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 13:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 12:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 11:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 10:00:00+00:00,London Westminster,pm25,13.0,µg/m³ +London,GB,2019-05-10 09:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 08:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 07:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 06:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 05:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 04:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 03:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 02:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-10 01:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-10 00:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-09 23:00:00+00:00,London Westminster,pm25,12.0,µg/m³ +London,GB,2019-05-09 22:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-09 21:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-09 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 19:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 18:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-09 10:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 09:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 08:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 05:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 04:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 03:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 02:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-09 00:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 23:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 21:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 20:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 19:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 18:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 17:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 16:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 15:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-08 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 07:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 06:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 05:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 04:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-08 03:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-08 02:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 01:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-08 00:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 23:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 21:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 20:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 19:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-07 18:00:00+00:00,London Westminster,pm25,11.0,µg/m³ +London,GB,2019-05-07 17:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 16:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 15:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 14:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 13:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 12:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 11:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 10:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 09:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 08:00:00+00:00,London Westminster,pm25,10.0,µg/m³ +London,GB,2019-05-07 07:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-07 06:00:00+00:00,London Westminster,pm25,9.0,µg/m³ +London,GB,2019-05-07 04:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-07 03:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-07 02:00:00+00:00,London Westminster,pm25,8.0,µg/m³ +London,GB,2019-05-07 01:00:00+00:00,London Westminster,pm25,8.0,µg/m³ diff --git a/doc/data/air_quality_stations.csv b/doc/data/air_quality_stations.csv new file mode 100644 index 0000000000000..9ab1a377dcdae --- /dev/null +++ b/doc/data/air_quality_stations.csv @@ -0,0 +1,67 @@ +location,coordinates.latitude,coordinates.longitude +BELAL01,51.23619,4.38522 +BELHB23,51.1703,4.341 +BELLD01,51.10998,5.00486 +BELLD02,51.12038,5.02155 +BELR833,51.32766,4.36226 +BELSA04,51.31393,4.40387 +BELWZ02,51.1928,5.22153 +BETM802,51.26099,4.4244 +BETN016,51.23365,5.16398 +BETR801,51.20966,4.43182 +BETR802,51.20952,4.43179 +BETR803,51.22863,4.42845 +BETR805,51.20823,4.42156 +BETR811,51.2521,4.49136 +BETR815,51.2147,4.33221 +BETR817,51.17713,4.41795 +BETR820,51.32042,4.44481 +BETR822,51.26429,4.34128 +BETR831,51.3488,4.33971 +BETR834,51.092,4.3801 +BETR891,51.25581,4.38536 +BETR893,51.28138,4.38577 +BETR894,51.2835,4.3495 +BETR897,51.25011,4.3421 +FR04004,48.89167,2.34667 +FR04012,48.82778,2.3275 +FR04014,48.83724,2.3939 +FR04014,48.83722,2.3939 +FR04031,48.86887,2.31194 +FR04031,48.86889,2.31194 +FR04037,48.82861,2.36028 +FR04060,48.8572,2.2933 +FR04071,48.8564,2.33528 +FR04071,48.85639,2.33528 +FR04118,48.87027,2.3325 +FR04118,48.87029,2.3325 +FR04131,48.87333,2.33028 +FR04135,48.83795,2.40806 +FR04135,48.83796,2.40806 +FR04141,48.85278,2.36056 +FR04141,48.85279,2.36056 +FR04143,48.859,2.351 +FR04143,48.85944,2.35111 +FR04179,48.83038,2.26989 +FR04329,48.8386,2.41279 +FR04329,48.83862,2.41278 +Camden Kerbside,51.54421,-0.17527 +Ealing Horn Lane,51.51895,-0.26562 +Haringey Roadside,51.5993,-0.06822 +London Bexley,51.46603,0.18481 +London Bloomsbury,51.52229,-0.12589 +London Eltham,51.45258,0.07077 +London Haringey Priory Park South,51.58413,-0.12525 +London Harlington,51.48879,-0.44161 +London Harrow Stanmore,51.61733,-0.29878 +London Hillingdon,51.49633,-0.46086 +London Marylebone Road,51.52253,-0.15461 +London N. Kensington,51.52105,-0.21349 +London Teddington,51.42099,-0.33965 +London Teddington Bushy Park,51.42529,-0.34561 +London Westminster,51.49467,-0.13193 +Southend-on-Sea,51.5442,0.67841 +Southwark A2 Old Kent Road,51.4805,-0.05955 +Thurrock,51.47707,0.31797 +Tower Hamlets Roadside,51.52253,-0.04216 +Groton Fort Griswold,41.3536,-72.0789 diff --git a/doc/data/titanic.csv b/doc/data/titanic.csv new file mode 100644 index 0000000000000..5cc466e97cf12 --- /dev/null +++ b/doc/data/titanic.csv @@ -0,0 +1,892 @@ +PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked +1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S +2,1,1,"Cumings, Mrs. John Bradley (Florence Briggs Thayer)",female,38,1,0,PC 17599,71.2833,C85,C +3,1,3,"Heikkinen, Miss. Laina",female,26,0,0,STON/O2. 3101282,7.925,,S +4,1,1,"Futrelle, Mrs. Jacques Heath (Lily May Peel)",female,35,1,0,113803,53.1,C123,S +5,0,3,"Allen, Mr. William Henry",male,35,0,0,373450,8.05,,S +6,0,3,"Moran, Mr. James",male,,0,0,330877,8.4583,,Q +7,0,1,"McCarthy, Mr. Timothy J",male,54,0,0,17463,51.8625,E46,S +8,0,3,"Palsson, Master. Gosta Leonard",male,2,3,1,349909,21.075,,S +9,1,3,"Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)",female,27,0,2,347742,11.1333,,S +10,1,2,"Nasser, Mrs. Nicholas (Adele Achem)",female,14,1,0,237736,30.0708,,C +11,1,3,"Sandstrom, Miss. Marguerite Rut",female,4,1,1,PP 9549,16.7,G6,S +12,1,1,"Bonnell, Miss. Elizabeth",female,58,0,0,113783,26.55,C103,S +13,0,3,"Saundercock, Mr. William Henry",male,20,0,0,A/5. 2151,8.05,,S +14,0,3,"Andersson, Mr. Anders Johan",male,39,1,5,347082,31.275,,S +15,0,3,"Vestrom, Miss. Hulda Amanda Adolfina",female,14,0,0,350406,7.8542,,S +16,1,2,"Hewlett, Mrs. (Mary D Kingcome) ",female,55,0,0,248706,16,,S +17,0,3,"Rice, Master. Eugene",male,2,4,1,382652,29.125,,Q +18,1,2,"Williams, Mr. Charles Eugene",male,,0,0,244373,13,,S +19,0,3,"Vander Planke, Mrs. Julius (Emelia Maria Vandemoortele)",female,31,1,0,345763,18,,S +20,1,3,"Masselmani, Mrs. Fatima",female,,0,0,2649,7.225,,C +21,0,2,"Fynney, Mr. Joseph J",male,35,0,0,239865,26,,S +22,1,2,"Beesley, Mr. Lawrence",male,34,0,0,248698,13,D56,S +23,1,3,"McGowan, Miss. Anna ""Annie""",female,15,0,0,330923,8.0292,,Q +24,1,1,"Sloper, Mr. William Thompson",male,28,0,0,113788,35.5,A6,S +25,0,3,"Palsson, Miss. Torborg Danira",female,8,3,1,349909,21.075,,S +26,1,3,"Asplund, Mrs. Carl Oscar (Selma Augusta Emilia Johansson)",female,38,1,5,347077,31.3875,,S +27,0,3,"Emir, Mr. Farred Chehab",male,,0,0,2631,7.225,,C +28,0,1,"Fortune, Mr. Charles Alexander",male,19,3,2,19950,263,C23 C25 C27,S +29,1,3,"O'Dwyer, Miss. Ellen ""Nellie""",female,,0,0,330959,7.8792,,Q +30,0,3,"Todoroff, Mr. Lalio",male,,0,0,349216,7.8958,,S +31,0,1,"Uruchurtu, Don. Manuel E",male,40,0,0,PC 17601,27.7208,,C +32,1,1,"Spencer, Mrs. William Augustus (Marie Eugenie)",female,,1,0,PC 17569,146.5208,B78,C +33,1,3,"Glynn, Miss. Mary Agatha",female,,0,0,335677,7.75,,Q +34,0,2,"Wheadon, Mr. Edward H",male,66,0,0,C.A. 24579,10.5,,S +35,0,1,"Meyer, Mr. Edgar Joseph",male,28,1,0,PC 17604,82.1708,,C +36,0,1,"Holverson, Mr. Alexander Oskar",male,42,1,0,113789,52,,S +37,1,3,"Mamee, Mr. Hanna",male,,0,0,2677,7.2292,,C +38,0,3,"Cann, Mr. Ernest Charles",male,21,0,0,A./5. 2152,8.05,,S +39,0,3,"Vander Planke, Miss. Augusta Maria",female,18,2,0,345764,18,,S +40,1,3,"Nicola-Yarred, Miss. Jamila",female,14,1,0,2651,11.2417,,C +41,0,3,"Ahlin, Mrs. Johan (Johanna Persdotter Larsson)",female,40,1,0,7546,9.475,,S +42,0,2,"Turpin, Mrs. William John Robert (Dorothy Ann Wonnacott)",female,27,1,0,11668,21,,S +43,0,3,"Kraeff, Mr. Theodor",male,,0,0,349253,7.8958,,C +44,1,2,"Laroche, Miss. Simonne Marie Anne Andree",female,3,1,2,SC/Paris 2123,41.5792,,C +45,1,3,"Devaney, Miss. Margaret Delia",female,19,0,0,330958,7.8792,,Q +46,0,3,"Rogers, Mr. William John",male,,0,0,S.C./A.4. 23567,8.05,,S +47,0,3,"Lennon, Mr. Denis",male,,1,0,370371,15.5,,Q +48,1,3,"O'Driscoll, Miss. Bridget",female,,0,0,14311,7.75,,Q +49,0,3,"Samaan, Mr. Youssef",male,,2,0,2662,21.6792,,C +50,0,3,"Arnold-Franchi, Mrs. Josef (Josefine Franchi)",female,18,1,0,349237,17.8,,S +51,0,3,"Panula, Master. Juha Niilo",male,7,4,1,3101295,39.6875,,S +52,0,3,"Nosworthy, Mr. Richard Cater",male,21,0,0,A/4. 39886,7.8,,S +53,1,1,"Harper, Mrs. Henry Sleeper (Myna Haxtun)",female,49,1,0,PC 17572,76.7292,D33,C +54,1,2,"Faunthorpe, Mrs. Lizzie (Elizabeth Anne Wilkinson)",female,29,1,0,2926,26,,S +55,0,1,"Ostby, Mr. Engelhart Cornelius",male,65,0,1,113509,61.9792,B30,C +56,1,1,"Woolner, Mr. Hugh",male,,0,0,19947,35.5,C52,S +57,1,2,"Rugg, Miss. Emily",female,21,0,0,C.A. 31026,10.5,,S +58,0,3,"Novel, Mr. Mansouer",male,28.5,0,0,2697,7.2292,,C +59,1,2,"West, Miss. Constance Mirium",female,5,1,2,C.A. 34651,27.75,,S +60,0,3,"Goodwin, Master. William Frederick",male,11,5,2,CA 2144,46.9,,S +61,0,3,"Sirayanian, Mr. Orsen",male,22,0,0,2669,7.2292,,C +62,1,1,"Icard, Miss. Amelie",female,38,0,0,113572,80,B28, +63,0,1,"Harris, Mr. Henry Birkhardt",male,45,1,0,36973,83.475,C83,S +64,0,3,"Skoog, Master. Harald",male,4,3,2,347088,27.9,,S +65,0,1,"Stewart, Mr. Albert A",male,,0,0,PC 17605,27.7208,,C +66,1,3,"Moubarek, Master. Gerios",male,,1,1,2661,15.2458,,C +67,1,2,"Nye, Mrs. (Elizabeth Ramell)",female,29,0,0,C.A. 29395,10.5,F33,S +68,0,3,"Crease, Mr. Ernest James",male,19,0,0,S.P. 3464,8.1583,,S +69,1,3,"Andersson, Miss. Erna Alexandra",female,17,4,2,3101281,7.925,,S +70,0,3,"Kink, Mr. Vincenz",male,26,2,0,315151,8.6625,,S +71,0,2,"Jenkin, Mr. Stephen Curnow",male,32,0,0,C.A. 33111,10.5,,S +72,0,3,"Goodwin, Miss. Lillian Amy",female,16,5,2,CA 2144,46.9,,S +73,0,2,"Hood, Mr. Ambrose Jr",male,21,0,0,S.O.C. 14879,73.5,,S +74,0,3,"Chronopoulos, Mr. Apostolos",male,26,1,0,2680,14.4542,,C +75,1,3,"Bing, Mr. Lee",male,32,0,0,1601,56.4958,,S +76,0,3,"Moen, Mr. Sigurd Hansen",male,25,0,0,348123,7.65,F G73,S +77,0,3,"Staneff, Mr. Ivan",male,,0,0,349208,7.8958,,S +78,0,3,"Moutal, Mr. Rahamin Haim",male,,0,0,374746,8.05,,S +79,1,2,"Caldwell, Master. Alden Gates",male,0.83,0,2,248738,29,,S +80,1,3,"Dowdell, Miss. Elizabeth",female,30,0,0,364516,12.475,,S +81,0,3,"Waelens, Mr. Achille",male,22,0,0,345767,9,,S +82,1,3,"Sheerlinck, Mr. Jan Baptist",male,29,0,0,345779,9.5,,S +83,1,3,"McDermott, Miss. Brigdet Delia",female,,0,0,330932,7.7875,,Q +84,0,1,"Carrau, Mr. Francisco M",male,28,0,0,113059,47.1,,S +85,1,2,"Ilett, Miss. Bertha",female,17,0,0,SO/C 14885,10.5,,S +86,1,3,"Backstrom, Mrs. Karl Alfred (Maria Mathilda Gustafsson)",female,33,3,0,3101278,15.85,,S +87,0,3,"Ford, Mr. William Neal",male,16,1,3,W./C. 6608,34.375,,S +88,0,3,"Slocovski, Mr. Selman Francis",male,,0,0,SOTON/OQ 392086,8.05,,S +89,1,1,"Fortune, Miss. Mabel Helen",female,23,3,2,19950,263,C23 C25 C27,S +90,0,3,"Celotti, Mr. Francesco",male,24,0,0,343275,8.05,,S +91,0,3,"Christmann, Mr. Emil",male,29,0,0,343276,8.05,,S +92,0,3,"Andreasson, Mr. Paul Edvin",male,20,0,0,347466,7.8542,,S +93,0,1,"Chaffee, Mr. Herbert Fuller",male,46,1,0,W.E.P. 5734,61.175,E31,S +94,0,3,"Dean, Mr. Bertram Frank",male,26,1,2,C.A. 2315,20.575,,S +95,0,3,"Coxon, Mr. Daniel",male,59,0,0,364500,7.25,,S +96,0,3,"Shorney, Mr. Charles Joseph",male,,0,0,374910,8.05,,S +97,0,1,"Goldschmidt, Mr. George B",male,71,0,0,PC 17754,34.6542,A5,C +98,1,1,"Greenfield, Mr. William Bertram",male,23,0,1,PC 17759,63.3583,D10 D12,C +99,1,2,"Doling, Mrs. John T (Ada Julia Bone)",female,34,0,1,231919,23,,S +100,0,2,"Kantor, Mr. Sinai",male,34,1,0,244367,26,,S +101,0,3,"Petranec, Miss. Matilda",female,28,0,0,349245,7.8958,,S +102,0,3,"Petroff, Mr. Pastcho (""Pentcho"")",male,,0,0,349215,7.8958,,S +103,0,1,"White, Mr. Richard Frasar",male,21,0,1,35281,77.2875,D26,S +104,0,3,"Johansson, Mr. Gustaf Joel",male,33,0,0,7540,8.6542,,S +105,0,3,"Gustafsson, Mr. Anders Vilhelm",male,37,2,0,3101276,7.925,,S +106,0,3,"Mionoff, Mr. Stoytcho",male,28,0,0,349207,7.8958,,S +107,1,3,"Salkjelsvik, Miss. Anna Kristine",female,21,0,0,343120,7.65,,S +108,1,3,"Moss, Mr. Albert Johan",male,,0,0,312991,7.775,,S +109,0,3,"Rekic, Mr. Tido",male,38,0,0,349249,7.8958,,S +110,1,3,"Moran, Miss. Bertha",female,,1,0,371110,24.15,,Q +111,0,1,"Porter, Mr. Walter Chamberlain",male,47,0,0,110465,52,C110,S +112,0,3,"Zabour, Miss. Hileni",female,14.5,1,0,2665,14.4542,,C +113,0,3,"Barton, Mr. David John",male,22,0,0,324669,8.05,,S +114,0,3,"Jussila, Miss. Katriina",female,20,1,0,4136,9.825,,S +115,0,3,"Attalah, Miss. Malake",female,17,0,0,2627,14.4583,,C +116,0,3,"Pekoniemi, Mr. Edvard",male,21,0,0,STON/O 2. 3101294,7.925,,S +117,0,3,"Connors, Mr. Patrick",male,70.5,0,0,370369,7.75,,Q +118,0,2,"Turpin, Mr. William John Robert",male,29,1,0,11668,21,,S +119,0,1,"Baxter, Mr. Quigg Edmond",male,24,0,1,PC 17558,247.5208,B58 B60,C +120,0,3,"Andersson, Miss. Ellis Anna Maria",female,2,4,2,347082,31.275,,S +121,0,2,"Hickman, Mr. Stanley George",male,21,2,0,S.O.C. 14879,73.5,,S +122,0,3,"Moore, Mr. Leonard Charles",male,,0,0,A4. 54510,8.05,,S +123,0,2,"Nasser, Mr. Nicholas",male,32.5,1,0,237736,30.0708,,C +124,1,2,"Webber, Miss. Susan",female,32.5,0,0,27267,13,E101,S +125,0,1,"White, Mr. Percival Wayland",male,54,0,1,35281,77.2875,D26,S +126,1,3,"Nicola-Yarred, Master. Elias",male,12,1,0,2651,11.2417,,C +127,0,3,"McMahon, Mr. Martin",male,,0,0,370372,7.75,,Q +128,1,3,"Madsen, Mr. Fridtjof Arne",male,24,0,0,C 17369,7.1417,,S +129,1,3,"Peter, Miss. Anna",female,,1,1,2668,22.3583,F E69,C +130,0,3,"Ekstrom, Mr. Johan",male,45,0,0,347061,6.975,,S +131,0,3,"Drazenoic, Mr. Jozef",male,33,0,0,349241,7.8958,,C +132,0,3,"Coelho, Mr. Domingos Fernandeo",male,20,0,0,SOTON/O.Q. 3101307,7.05,,S +133,0,3,"Robins, Mrs. Alexander A (Grace Charity Laury)",female,47,1,0,A/5. 3337,14.5,,S +134,1,2,"Weisz, Mrs. Leopold (Mathilde Francoise Pede)",female,29,1,0,228414,26,,S +135,0,2,"Sobey, Mr. Samuel James Hayden",male,25,0,0,C.A. 29178,13,,S +136,0,2,"Richard, Mr. Emile",male,23,0,0,SC/PARIS 2133,15.0458,,C +137,1,1,"Newsom, Miss. Helen Monypeny",female,19,0,2,11752,26.2833,D47,S +138,0,1,"Futrelle, Mr. Jacques Heath",male,37,1,0,113803,53.1,C123,S +139,0,3,"Osen, Mr. Olaf Elon",male,16,0,0,7534,9.2167,,S +140,0,1,"Giglio, Mr. Victor",male,24,0,0,PC 17593,79.2,B86,C +141,0,3,"Boulos, Mrs. Joseph (Sultana)",female,,0,2,2678,15.2458,,C +142,1,3,"Nysten, Miss. Anna Sofia",female,22,0,0,347081,7.75,,S +143,1,3,"Hakkarainen, Mrs. Pekka Pietari (Elin Matilda Dolck)",female,24,1,0,STON/O2. 3101279,15.85,,S +144,0,3,"Burke, Mr. Jeremiah",male,19,0,0,365222,6.75,,Q +145,0,2,"Andrew, Mr. Edgardo Samuel",male,18,0,0,231945,11.5,,S +146,0,2,"Nicholls, Mr. Joseph Charles",male,19,1,1,C.A. 33112,36.75,,S +147,1,3,"Andersson, Mr. August Edvard (""Wennerstrom"")",male,27,0,0,350043,7.7958,,S +148,0,3,"Ford, Miss. Robina Maggie ""Ruby""",female,9,2,2,W./C. 6608,34.375,,S +149,0,2,"Navratil, Mr. Michel (""Louis M Hoffman"")",male,36.5,0,2,230080,26,F2,S +150,0,2,"Byles, Rev. Thomas Roussel Davids",male,42,0,0,244310,13,,S +151,0,2,"Bateman, Rev. Robert James",male,51,0,0,S.O.P. 1166,12.525,,S +152,1,1,"Pears, Mrs. Thomas (Edith Wearne)",female,22,1,0,113776,66.6,C2,S +153,0,3,"Meo, Mr. Alfonzo",male,55.5,0,0,A.5. 11206,8.05,,S +154,0,3,"van Billiard, Mr. Austin Blyler",male,40.5,0,2,A/5. 851,14.5,,S +155,0,3,"Olsen, Mr. Ole Martin",male,,0,0,Fa 265302,7.3125,,S +156,0,1,"Williams, Mr. Charles Duane",male,51,0,1,PC 17597,61.3792,,C +157,1,3,"Gilnagh, Miss. Katherine ""Katie""",female,16,0,0,35851,7.7333,,Q +158,0,3,"Corn, Mr. Harry",male,30,0,0,SOTON/OQ 392090,8.05,,S +159,0,3,"Smiljanic, Mr. Mile",male,,0,0,315037,8.6625,,S +160,0,3,"Sage, Master. Thomas Henry",male,,8,2,CA. 2343,69.55,,S +161,0,3,"Cribb, Mr. John Hatfield",male,44,0,1,371362,16.1,,S +162,1,2,"Watt, Mrs. James (Elizabeth ""Bessie"" Inglis Milne)",female,40,0,0,C.A. 33595,15.75,,S +163,0,3,"Bengtsson, Mr. John Viktor",male,26,0,0,347068,7.775,,S +164,0,3,"Calic, Mr. Jovo",male,17,0,0,315093,8.6625,,S +165,0,3,"Panula, Master. Eino Viljami",male,1,4,1,3101295,39.6875,,S +166,1,3,"Goldsmith, Master. Frank John William ""Frankie""",male,9,0,2,363291,20.525,,S +167,1,1,"Chibnall, Mrs. (Edith Martha Bowerman)",female,,0,1,113505,55,E33,S +168,0,3,"Skoog, Mrs. William (Anna Bernhardina Karlsson)",female,45,1,4,347088,27.9,,S +169,0,1,"Baumann, Mr. John D",male,,0,0,PC 17318,25.925,,S +170,0,3,"Ling, Mr. Lee",male,28,0,0,1601,56.4958,,S +171,0,1,"Van der hoef, Mr. Wyckoff",male,61,0,0,111240,33.5,B19,S +172,0,3,"Rice, Master. Arthur",male,4,4,1,382652,29.125,,Q +173,1,3,"Johnson, Miss. Eleanor Ileen",female,1,1,1,347742,11.1333,,S +174,0,3,"Sivola, Mr. Antti Wilhelm",male,21,0,0,STON/O 2. 3101280,7.925,,S +175,0,1,"Smith, Mr. James Clinch",male,56,0,0,17764,30.6958,A7,C +176,0,3,"Klasen, Mr. Klas Albin",male,18,1,1,350404,7.8542,,S +177,0,3,"Lefebre, Master. Henry Forbes",male,,3,1,4133,25.4667,,S +178,0,1,"Isham, Miss. Ann Elizabeth",female,50,0,0,PC 17595,28.7125,C49,C +179,0,2,"Hale, Mr. Reginald",male,30,0,0,250653,13,,S +180,0,3,"Leonard, Mr. Lionel",male,36,0,0,LINE,0,,S +181,0,3,"Sage, Miss. Constance Gladys",female,,8,2,CA. 2343,69.55,,S +182,0,2,"Pernot, Mr. Rene",male,,0,0,SC/PARIS 2131,15.05,,C +183,0,3,"Asplund, Master. Clarence Gustaf Hugo",male,9,4,2,347077,31.3875,,S +184,1,2,"Becker, Master. Richard F",male,1,2,1,230136,39,F4,S +185,1,3,"Kink-Heilmann, Miss. Luise Gretchen",female,4,0,2,315153,22.025,,S +186,0,1,"Rood, Mr. Hugh Roscoe",male,,0,0,113767,50,A32,S +187,1,3,"O'Brien, Mrs. Thomas (Johanna ""Hannah"" Godfrey)",female,,1,0,370365,15.5,,Q +188,1,1,"Romaine, Mr. Charles Hallace (""Mr C Rolmane"")",male,45,0,0,111428,26.55,,S +189,0,3,"Bourke, Mr. John",male,40,1,1,364849,15.5,,Q +190,0,3,"Turcin, Mr. Stjepan",male,36,0,0,349247,7.8958,,S +191,1,2,"Pinsky, Mrs. (Rosa)",female,32,0,0,234604,13,,S +192,0,2,"Carbines, Mr. William",male,19,0,0,28424,13,,S +193,1,3,"Andersen-Jensen, Miss. Carla Christine Nielsine",female,19,1,0,350046,7.8542,,S +194,1,2,"Navratil, Master. Michel M",male,3,1,1,230080,26,F2,S +195,1,1,"Brown, Mrs. James Joseph (Margaret Tobin)",female,44,0,0,PC 17610,27.7208,B4,C +196,1,1,"Lurette, Miss. Elise",female,58,0,0,PC 17569,146.5208,B80,C +197,0,3,"Mernagh, Mr. Robert",male,,0,0,368703,7.75,,Q +198,0,3,"Olsen, Mr. Karl Siegwart Andreas",male,42,0,1,4579,8.4042,,S +199,1,3,"Madigan, Miss. Margaret ""Maggie""",female,,0,0,370370,7.75,,Q +200,0,2,"Yrois, Miss. Henriette (""Mrs Harbeck"")",female,24,0,0,248747,13,,S +201,0,3,"Vande Walle, Mr. Nestor Cyriel",male,28,0,0,345770,9.5,,S +202,0,3,"Sage, Mr. Frederick",male,,8,2,CA. 2343,69.55,,S +203,0,3,"Johanson, Mr. Jakob Alfred",male,34,0,0,3101264,6.4958,,S +204,0,3,"Youseff, Mr. Gerious",male,45.5,0,0,2628,7.225,,C +205,1,3,"Cohen, Mr. Gurshon ""Gus""",male,18,0,0,A/5 3540,8.05,,S +206,0,3,"Strom, Miss. Telma Matilda",female,2,0,1,347054,10.4625,G6,S +207,0,3,"Backstrom, Mr. Karl Alfred",male,32,1,0,3101278,15.85,,S +208,1,3,"Albimona, Mr. Nassef Cassem",male,26,0,0,2699,18.7875,,C +209,1,3,"Carr, Miss. Helen ""Ellen""",female,16,0,0,367231,7.75,,Q +210,1,1,"Blank, Mr. Henry",male,40,0,0,112277,31,A31,C +211,0,3,"Ali, Mr. Ahmed",male,24,0,0,SOTON/O.Q. 3101311,7.05,,S +212,1,2,"Cameron, Miss. Clear Annie",female,35,0,0,F.C.C. 13528,21,,S +213,0,3,"Perkin, Mr. John Henry",male,22,0,0,A/5 21174,7.25,,S +214,0,2,"Givard, Mr. Hans Kristensen",male,30,0,0,250646,13,,S +215,0,3,"Kiernan, Mr. Philip",male,,1,0,367229,7.75,,Q +216,1,1,"Newell, Miss. Madeleine",female,31,1,0,35273,113.275,D36,C +217,1,3,"Honkanen, Miss. Eliina",female,27,0,0,STON/O2. 3101283,7.925,,S +218,0,2,"Jacobsohn, Mr. Sidney Samuel",male,42,1,0,243847,27,,S +219,1,1,"Bazzani, Miss. Albina",female,32,0,0,11813,76.2917,D15,C +220,0,2,"Harris, Mr. Walter",male,30,0,0,W/C 14208,10.5,,S +221,1,3,"Sunderland, Mr. Victor Francis",male,16,0,0,SOTON/OQ 392089,8.05,,S +222,0,2,"Bracken, Mr. James H",male,27,0,0,220367,13,,S +223,0,3,"Green, Mr. George Henry",male,51,0,0,21440,8.05,,S +224,0,3,"Nenkoff, Mr. Christo",male,,0,0,349234,7.8958,,S +225,1,1,"Hoyt, Mr. Frederick Maxfield",male,38,1,0,19943,90,C93,S +226,0,3,"Berglund, Mr. Karl Ivar Sven",male,22,0,0,PP 4348,9.35,,S +227,1,2,"Mellors, Mr. William John",male,19,0,0,SW/PP 751,10.5,,S +228,0,3,"Lovell, Mr. John Hall (""Henry"")",male,20.5,0,0,A/5 21173,7.25,,S +229,0,2,"Fahlstrom, Mr. Arne Jonas",male,18,0,0,236171,13,,S +230,0,3,"Lefebre, Miss. Mathilde",female,,3,1,4133,25.4667,,S +231,1,1,"Harris, Mrs. Henry Birkhardt (Irene Wallach)",female,35,1,0,36973,83.475,C83,S +232,0,3,"Larsson, Mr. Bengt Edvin",male,29,0,0,347067,7.775,,S +233,0,2,"Sjostedt, Mr. Ernst Adolf",male,59,0,0,237442,13.5,,S +234,1,3,"Asplund, Miss. Lillian Gertrud",female,5,4,2,347077,31.3875,,S +235,0,2,"Leyson, Mr. Robert William Norman",male,24,0,0,C.A. 29566,10.5,,S +236,0,3,"Harknett, Miss. Alice Phoebe",female,,0,0,W./C. 6609,7.55,,S +237,0,2,"Hold, Mr. Stephen",male,44,1,0,26707,26,,S +238,1,2,"Collyer, Miss. Marjorie ""Lottie""",female,8,0,2,C.A. 31921,26.25,,S +239,0,2,"Pengelly, Mr. Frederick William",male,19,0,0,28665,10.5,,S +240,0,2,"Hunt, Mr. George Henry",male,33,0,0,SCO/W 1585,12.275,,S +241,0,3,"Zabour, Miss. Thamine",female,,1,0,2665,14.4542,,C +242,1,3,"Murphy, Miss. Katherine ""Kate""",female,,1,0,367230,15.5,,Q +243,0,2,"Coleridge, Mr. Reginald Charles",male,29,0,0,W./C. 14263,10.5,,S +244,0,3,"Maenpaa, Mr. Matti Alexanteri",male,22,0,0,STON/O 2. 3101275,7.125,,S +245,0,3,"Attalah, Mr. Sleiman",male,30,0,0,2694,7.225,,C +246,0,1,"Minahan, Dr. William Edward",male,44,2,0,19928,90,C78,Q +247,0,3,"Lindahl, Miss. Agda Thorilda Viktoria",female,25,0,0,347071,7.775,,S +248,1,2,"Hamalainen, Mrs. William (Anna)",female,24,0,2,250649,14.5,,S +249,1,1,"Beckwith, Mr. Richard Leonard",male,37,1,1,11751,52.5542,D35,S +250,0,2,"Carter, Rev. Ernest Courtenay",male,54,1,0,244252,26,,S +251,0,3,"Reed, Mr. James George",male,,0,0,362316,7.25,,S +252,0,3,"Strom, Mrs. Wilhelm (Elna Matilda Persson)",female,29,1,1,347054,10.4625,G6,S +253,0,1,"Stead, Mr. William Thomas",male,62,0,0,113514,26.55,C87,S +254,0,3,"Lobb, Mr. William Arthur",male,30,1,0,A/5. 3336,16.1,,S +255,0,3,"Rosblom, Mrs. Viktor (Helena Wilhelmina)",female,41,0,2,370129,20.2125,,S +256,1,3,"Touma, Mrs. Darwis (Hanne Youssef Razi)",female,29,0,2,2650,15.2458,,C +257,1,1,"Thorne, Mrs. Gertrude Maybelle",female,,0,0,PC 17585,79.2,,C +258,1,1,"Cherry, Miss. Gladys",female,30,0,0,110152,86.5,B77,S +259,1,1,"Ward, Miss. Anna",female,35,0,0,PC 17755,512.3292,,C +260,1,2,"Parrish, Mrs. (Lutie Davis)",female,50,0,1,230433,26,,S +261,0,3,"Smith, Mr. Thomas",male,,0,0,384461,7.75,,Q +262,1,3,"Asplund, Master. Edvin Rojj Felix",male,3,4,2,347077,31.3875,,S +263,0,1,"Taussig, Mr. Emil",male,52,1,1,110413,79.65,E67,S +264,0,1,"Harrison, Mr. William",male,40,0,0,112059,0,B94,S +265,0,3,"Henry, Miss. Delia",female,,0,0,382649,7.75,,Q +266,0,2,"Reeves, Mr. David",male,36,0,0,C.A. 17248,10.5,,S +267,0,3,"Panula, Mr. Ernesti Arvid",male,16,4,1,3101295,39.6875,,S +268,1,3,"Persson, Mr. Ernst Ulrik",male,25,1,0,347083,7.775,,S +269,1,1,"Graham, Mrs. William Thompson (Edith Junkins)",female,58,0,1,PC 17582,153.4625,C125,S +270,1,1,"Bissette, Miss. Amelia",female,35,0,0,PC 17760,135.6333,C99,S +271,0,1,"Cairns, Mr. Alexander",male,,0,0,113798,31,,S +272,1,3,"Tornquist, Mr. William Henry",male,25,0,0,LINE,0,,S +273,1,2,"Mellinger, Mrs. (Elizabeth Anne Maidment)",female,41,0,1,250644,19.5,,S +274,0,1,"Natsch, Mr. Charles H",male,37,0,1,PC 17596,29.7,C118,C +275,1,3,"Healy, Miss. Hanora ""Nora""",female,,0,0,370375,7.75,,Q +276,1,1,"Andrews, Miss. Kornelia Theodosia",female,63,1,0,13502,77.9583,D7,S +277,0,3,"Lindblom, Miss. Augusta Charlotta",female,45,0,0,347073,7.75,,S +278,0,2,"Parkes, Mr. Francis ""Frank""",male,,0,0,239853,0,,S +279,0,3,"Rice, Master. Eric",male,7,4,1,382652,29.125,,Q +280,1,3,"Abbott, Mrs. Stanton (Rosa Hunt)",female,35,1,1,C.A. 2673,20.25,,S +281,0,3,"Duane, Mr. Frank",male,65,0,0,336439,7.75,,Q +282,0,3,"Olsson, Mr. Nils Johan Goransson",male,28,0,0,347464,7.8542,,S +283,0,3,"de Pelsmaeker, Mr. Alfons",male,16,0,0,345778,9.5,,S +284,1,3,"Dorking, Mr. Edward Arthur",male,19,0,0,A/5. 10482,8.05,,S +285,0,1,"Smith, Mr. Richard William",male,,0,0,113056,26,A19,S +286,0,3,"Stankovic, Mr. Ivan",male,33,0,0,349239,8.6625,,C +287,1,3,"de Mulder, Mr. Theodore",male,30,0,0,345774,9.5,,S +288,0,3,"Naidenoff, Mr. Penko",male,22,0,0,349206,7.8958,,S +289,1,2,"Hosono, Mr. Masabumi",male,42,0,0,237798,13,,S +290,1,3,"Connolly, Miss. Kate",female,22,0,0,370373,7.75,,Q +291,1,1,"Barber, Miss. Ellen ""Nellie""",female,26,0,0,19877,78.85,,S +292,1,1,"Bishop, Mrs. Dickinson H (Helen Walton)",female,19,1,0,11967,91.0792,B49,C +293,0,2,"Levy, Mr. Rene Jacques",male,36,0,0,SC/Paris 2163,12.875,D,C +294,0,3,"Haas, Miss. Aloisia",female,24,0,0,349236,8.85,,S +295,0,3,"Mineff, Mr. Ivan",male,24,0,0,349233,7.8958,,S +296,0,1,"Lewy, Mr. Ervin G",male,,0,0,PC 17612,27.7208,,C +297,0,3,"Hanna, Mr. Mansour",male,23.5,0,0,2693,7.2292,,C +298,0,1,"Allison, Miss. Helen Loraine",female,2,1,2,113781,151.55,C22 C26,S +299,1,1,"Saalfeld, Mr. Adolphe",male,,0,0,19988,30.5,C106,S +300,1,1,"Baxter, Mrs. James (Helene DeLaudeniere Chaput)",female,50,0,1,PC 17558,247.5208,B58 B60,C +301,1,3,"Kelly, Miss. Anna Katherine ""Annie Kate""",female,,0,0,9234,7.75,,Q +302,1,3,"McCoy, Mr. Bernard",male,,2,0,367226,23.25,,Q +303,0,3,"Johnson, Mr. William Cahoone Jr",male,19,0,0,LINE,0,,S +304,1,2,"Keane, Miss. Nora A",female,,0,0,226593,12.35,E101,Q +305,0,3,"Williams, Mr. Howard Hugh ""Harry""",male,,0,0,A/5 2466,8.05,,S +306,1,1,"Allison, Master. Hudson Trevor",male,0.92,1,2,113781,151.55,C22 C26,S +307,1,1,"Fleming, Miss. Margaret",female,,0,0,17421,110.8833,,C +308,1,1,"Penasco y Castellana, Mrs. Victor de Satode (Maria Josefa Perez de Soto y Vallejo)",female,17,1,0,PC 17758,108.9,C65,C +309,0,2,"Abelson, Mr. Samuel",male,30,1,0,P/PP 3381,24,,C +310,1,1,"Francatelli, Miss. Laura Mabel",female,30,0,0,PC 17485,56.9292,E36,C +311,1,1,"Hays, Miss. Margaret Bechstein",female,24,0,0,11767,83.1583,C54,C +312,1,1,"Ryerson, Miss. Emily Borie",female,18,2,2,PC 17608,262.375,B57 B59 B63 B66,C +313,0,2,"Lahtinen, Mrs. William (Anna Sylfven)",female,26,1,1,250651,26,,S +314,0,3,"Hendekovic, Mr. Ignjac",male,28,0,0,349243,7.8958,,S +315,0,2,"Hart, Mr. Benjamin",male,43,1,1,F.C.C. 13529,26.25,,S +316,1,3,"Nilsson, Miss. Helmina Josefina",female,26,0,0,347470,7.8542,,S +317,1,2,"Kantor, Mrs. Sinai (Miriam Sternin)",female,24,1,0,244367,26,,S +318,0,2,"Moraweck, Dr. Ernest",male,54,0,0,29011,14,,S +319,1,1,"Wick, Miss. Mary Natalie",female,31,0,2,36928,164.8667,C7,S +320,1,1,"Spedden, Mrs. Frederic Oakley (Margaretta Corning Stone)",female,40,1,1,16966,134.5,E34,C +321,0,3,"Dennis, Mr. Samuel",male,22,0,0,A/5 21172,7.25,,S +322,0,3,"Danoff, Mr. Yoto",male,27,0,0,349219,7.8958,,S +323,1,2,"Slayter, Miss. Hilda Mary",female,30,0,0,234818,12.35,,Q +324,1,2,"Caldwell, Mrs. Albert Francis (Sylvia Mae Harbaugh)",female,22,1,1,248738,29,,S +325,0,3,"Sage, Mr. George John Jr",male,,8,2,CA. 2343,69.55,,S +326,1,1,"Young, Miss. Marie Grice",female,36,0,0,PC 17760,135.6333,C32,C +327,0,3,"Nysveen, Mr. Johan Hansen",male,61,0,0,345364,6.2375,,S +328,1,2,"Ball, Mrs. (Ada E Hall)",female,36,0,0,28551,13,D,S +329,1,3,"Goldsmith, Mrs. Frank John (Emily Alice Brown)",female,31,1,1,363291,20.525,,S +330,1,1,"Hippach, Miss. Jean Gertrude",female,16,0,1,111361,57.9792,B18,C +331,1,3,"McCoy, Miss. Agnes",female,,2,0,367226,23.25,,Q +332,0,1,"Partner, Mr. Austen",male,45.5,0,0,113043,28.5,C124,S +333,0,1,"Graham, Mr. George Edward",male,38,0,1,PC 17582,153.4625,C91,S +334,0,3,"Vander Planke, Mr. Leo Edmondus",male,16,2,0,345764,18,,S +335,1,1,"Frauenthal, Mrs. Henry William (Clara Heinsheimer)",female,,1,0,PC 17611,133.65,,S +336,0,3,"Denkoff, Mr. Mitto",male,,0,0,349225,7.8958,,S +337,0,1,"Pears, Mr. Thomas Clinton",male,29,1,0,113776,66.6,C2,S +338,1,1,"Burns, Miss. Elizabeth Margaret",female,41,0,0,16966,134.5,E40,C +339,1,3,"Dahl, Mr. Karl Edwart",male,45,0,0,7598,8.05,,S +340,0,1,"Blackwell, Mr. Stephen Weart",male,45,0,0,113784,35.5,T,S +341,1,2,"Navratil, Master. Edmond Roger",male,2,1,1,230080,26,F2,S +342,1,1,"Fortune, Miss. Alice Elizabeth",female,24,3,2,19950,263,C23 C25 C27,S +343,0,2,"Collander, Mr. Erik Gustaf",male,28,0,0,248740,13,,S +344,0,2,"Sedgwick, Mr. Charles Frederick Waddington",male,25,0,0,244361,13,,S +345,0,2,"Fox, Mr. Stanley Hubert",male,36,0,0,229236,13,,S +346,1,2,"Brown, Miss. Amelia ""Mildred""",female,24,0,0,248733,13,F33,S +347,1,2,"Smith, Miss. Marion Elsie",female,40,0,0,31418,13,,S +348,1,3,"Davison, Mrs. Thomas Henry (Mary E Finck)",female,,1,0,386525,16.1,,S +349,1,3,"Coutts, Master. William Loch ""William""",male,3,1,1,C.A. 37671,15.9,,S +350,0,3,"Dimic, Mr. Jovan",male,42,0,0,315088,8.6625,,S +351,0,3,"Odahl, Mr. Nils Martin",male,23,0,0,7267,9.225,,S +352,0,1,"Williams-Lambert, Mr. Fletcher Fellows",male,,0,0,113510,35,C128,S +353,0,3,"Elias, Mr. Tannous",male,15,1,1,2695,7.2292,,C +354,0,3,"Arnold-Franchi, Mr. Josef",male,25,1,0,349237,17.8,,S +355,0,3,"Yousif, Mr. Wazli",male,,0,0,2647,7.225,,C +356,0,3,"Vanden Steen, Mr. Leo Peter",male,28,0,0,345783,9.5,,S +357,1,1,"Bowerman, Miss. Elsie Edith",female,22,0,1,113505,55,E33,S +358,0,2,"Funk, Miss. Annie Clemmer",female,38,0,0,237671,13,,S +359,1,3,"McGovern, Miss. Mary",female,,0,0,330931,7.8792,,Q +360,1,3,"Mockler, Miss. Helen Mary ""Ellie""",female,,0,0,330980,7.8792,,Q +361,0,3,"Skoog, Mr. Wilhelm",male,40,1,4,347088,27.9,,S +362,0,2,"del Carlo, Mr. Sebastiano",male,29,1,0,SC/PARIS 2167,27.7208,,C +363,0,3,"Barbara, Mrs. (Catherine David)",female,45,0,1,2691,14.4542,,C +364,0,3,"Asim, Mr. Adola",male,35,0,0,SOTON/O.Q. 3101310,7.05,,S +365,0,3,"O'Brien, Mr. Thomas",male,,1,0,370365,15.5,,Q +366,0,3,"Adahl, Mr. Mauritz Nils Martin",male,30,0,0,C 7076,7.25,,S +367,1,1,"Warren, Mrs. Frank Manley (Anna Sophia Atkinson)",female,60,1,0,110813,75.25,D37,C +368,1,3,"Moussa, Mrs. (Mantoura Boulos)",female,,0,0,2626,7.2292,,C +369,1,3,"Jermyn, Miss. Annie",female,,0,0,14313,7.75,,Q +370,1,1,"Aubart, Mme. Leontine Pauline",female,24,0,0,PC 17477,69.3,B35,C +371,1,1,"Harder, Mr. George Achilles",male,25,1,0,11765,55.4417,E50,C +372,0,3,"Wiklund, Mr. Jakob Alfred",male,18,1,0,3101267,6.4958,,S +373,0,3,"Beavan, Mr. William Thomas",male,19,0,0,323951,8.05,,S +374,0,1,"Ringhini, Mr. Sante",male,22,0,0,PC 17760,135.6333,,C +375,0,3,"Palsson, Miss. Stina Viola",female,3,3,1,349909,21.075,,S +376,1,1,"Meyer, Mrs. Edgar Joseph (Leila Saks)",female,,1,0,PC 17604,82.1708,,C +377,1,3,"Landergren, Miss. Aurora Adelia",female,22,0,0,C 7077,7.25,,S +378,0,1,"Widener, Mr. Harry Elkins",male,27,0,2,113503,211.5,C82,C +379,0,3,"Betros, Mr. Tannous",male,20,0,0,2648,4.0125,,C +380,0,3,"Gustafsson, Mr. Karl Gideon",male,19,0,0,347069,7.775,,S +381,1,1,"Bidois, Miss. Rosalie",female,42,0,0,PC 17757,227.525,,C +382,1,3,"Nakid, Miss. Maria (""Mary"")",female,1,0,2,2653,15.7417,,C +383,0,3,"Tikkanen, Mr. Juho",male,32,0,0,STON/O 2. 3101293,7.925,,S +384,1,1,"Holverson, Mrs. Alexander Oskar (Mary Aline Towner)",female,35,1,0,113789,52,,S +385,0,3,"Plotcharsky, Mr. Vasil",male,,0,0,349227,7.8958,,S +386,0,2,"Davies, Mr. Charles Henry",male,18,0,0,S.O.C. 14879,73.5,,S +387,0,3,"Goodwin, Master. Sidney Leonard",male,1,5,2,CA 2144,46.9,,S +388,1,2,"Buss, Miss. Kate",female,36,0,0,27849,13,,S +389,0,3,"Sadlier, Mr. Matthew",male,,0,0,367655,7.7292,,Q +390,1,2,"Lehmann, Miss. Bertha",female,17,0,0,SC 1748,12,,C +391,1,1,"Carter, Mr. William Ernest",male,36,1,2,113760,120,B96 B98,S +392,1,3,"Jansson, Mr. Carl Olof",male,21,0,0,350034,7.7958,,S +393,0,3,"Gustafsson, Mr. Johan Birger",male,28,2,0,3101277,7.925,,S +394,1,1,"Newell, Miss. Marjorie",female,23,1,0,35273,113.275,D36,C +395,1,3,"Sandstrom, Mrs. Hjalmar (Agnes Charlotta Bengtsson)",female,24,0,2,PP 9549,16.7,G6,S +396,0,3,"Johansson, Mr. Erik",male,22,0,0,350052,7.7958,,S +397,0,3,"Olsson, Miss. Elina",female,31,0,0,350407,7.8542,,S +398,0,2,"McKane, Mr. Peter David",male,46,0,0,28403,26,,S +399,0,2,"Pain, Dr. Alfred",male,23,0,0,244278,10.5,,S +400,1,2,"Trout, Mrs. William H (Jessie L)",female,28,0,0,240929,12.65,,S +401,1,3,"Niskanen, Mr. Juha",male,39,0,0,STON/O 2. 3101289,7.925,,S +402,0,3,"Adams, Mr. John",male,26,0,0,341826,8.05,,S +403,0,3,"Jussila, Miss. Mari Aina",female,21,1,0,4137,9.825,,S +404,0,3,"Hakkarainen, Mr. Pekka Pietari",male,28,1,0,STON/O2. 3101279,15.85,,S +405,0,3,"Oreskovic, Miss. Marija",female,20,0,0,315096,8.6625,,S +406,0,2,"Gale, Mr. Shadrach",male,34,1,0,28664,21,,S +407,0,3,"Widegren, Mr. Carl/Charles Peter",male,51,0,0,347064,7.75,,S +408,1,2,"Richards, Master. William Rowe",male,3,1,1,29106,18.75,,S +409,0,3,"Birkeland, Mr. Hans Martin Monsen",male,21,0,0,312992,7.775,,S +410,0,3,"Lefebre, Miss. Ida",female,,3,1,4133,25.4667,,S +411,0,3,"Sdycoff, Mr. Todor",male,,0,0,349222,7.8958,,S +412,0,3,"Hart, Mr. Henry",male,,0,0,394140,6.8583,,Q +413,1,1,"Minahan, Miss. Daisy E",female,33,1,0,19928,90,C78,Q +414,0,2,"Cunningham, Mr. Alfred Fleming",male,,0,0,239853,0,,S +415,1,3,"Sundman, Mr. Johan Julian",male,44,0,0,STON/O 2. 3101269,7.925,,S +416,0,3,"Meek, Mrs. Thomas (Annie Louise Rowley)",female,,0,0,343095,8.05,,S +417,1,2,"Drew, Mrs. James Vivian (Lulu Thorne Christian)",female,34,1,1,28220,32.5,,S +418,1,2,"Silven, Miss. Lyyli Karoliina",female,18,0,2,250652,13,,S +419,0,2,"Matthews, Mr. William John",male,30,0,0,28228,13,,S +420,0,3,"Van Impe, Miss. Catharina",female,10,0,2,345773,24.15,,S +421,0,3,"Gheorgheff, Mr. Stanio",male,,0,0,349254,7.8958,,C +422,0,3,"Charters, Mr. David",male,21,0,0,A/5. 13032,7.7333,,Q +423,0,3,"Zimmerman, Mr. Leo",male,29,0,0,315082,7.875,,S +424,0,3,"Danbom, Mrs. Ernst Gilbert (Anna Sigrid Maria Brogren)",female,28,1,1,347080,14.4,,S +425,0,3,"Rosblom, Mr. Viktor Richard",male,18,1,1,370129,20.2125,,S +426,0,3,"Wiseman, Mr. Phillippe",male,,0,0,A/4. 34244,7.25,,S +427,1,2,"Clarke, Mrs. Charles V (Ada Maria Winfield)",female,28,1,0,2003,26,,S +428,1,2,"Phillips, Miss. Kate Florence (""Mrs Kate Louise Phillips Marshall"")",female,19,0,0,250655,26,,S +429,0,3,"Flynn, Mr. James",male,,0,0,364851,7.75,,Q +430,1,3,"Pickard, Mr. Berk (Berk Trembisky)",male,32,0,0,SOTON/O.Q. 392078,8.05,E10,S +431,1,1,"Bjornstrom-Steffansson, Mr. Mauritz Hakan",male,28,0,0,110564,26.55,C52,S +432,1,3,"Thorneycroft, Mrs. Percival (Florence Kate White)",female,,1,0,376564,16.1,,S +433,1,2,"Louch, Mrs. Charles Alexander (Alice Adelaide Slow)",female,42,1,0,SC/AH 3085,26,,S +434,0,3,"Kallio, Mr. Nikolai Erland",male,17,0,0,STON/O 2. 3101274,7.125,,S +435,0,1,"Silvey, Mr. William Baird",male,50,1,0,13507,55.9,E44,S +436,1,1,"Carter, Miss. Lucile Polk",female,14,1,2,113760,120,B96 B98,S +437,0,3,"Ford, Miss. Doolina Margaret ""Daisy""",female,21,2,2,W./C. 6608,34.375,,S +438,1,2,"Richards, Mrs. Sidney (Emily Hocking)",female,24,2,3,29106,18.75,,S +439,0,1,"Fortune, Mr. Mark",male,64,1,4,19950,263,C23 C25 C27,S +440,0,2,"Kvillner, Mr. Johan Henrik Johannesson",male,31,0,0,C.A. 18723,10.5,,S +441,1,2,"Hart, Mrs. Benjamin (Esther Ada Bloomfield)",female,45,1,1,F.C.C. 13529,26.25,,S +442,0,3,"Hampe, Mr. Leon",male,20,0,0,345769,9.5,,S +443,0,3,"Petterson, Mr. Johan Emil",male,25,1,0,347076,7.775,,S +444,1,2,"Reynaldo, Ms. Encarnacion",female,28,0,0,230434,13,,S +445,1,3,"Johannesen-Bratthammer, Mr. Bernt",male,,0,0,65306,8.1125,,S +446,1,1,"Dodge, Master. Washington",male,4,0,2,33638,81.8583,A34,S +447,1,2,"Mellinger, Miss. Madeleine Violet",female,13,0,1,250644,19.5,,S +448,1,1,"Seward, Mr. Frederic Kimber",male,34,0,0,113794,26.55,,S +449,1,3,"Baclini, Miss. Marie Catherine",female,5,2,1,2666,19.2583,,C +450,1,1,"Peuchen, Major. Arthur Godfrey",male,52,0,0,113786,30.5,C104,S +451,0,2,"West, Mr. Edwy Arthur",male,36,1,2,C.A. 34651,27.75,,S +452,0,3,"Hagland, Mr. Ingvald Olai Olsen",male,,1,0,65303,19.9667,,S +453,0,1,"Foreman, Mr. Benjamin Laventall",male,30,0,0,113051,27.75,C111,C +454,1,1,"Goldenberg, Mr. Samuel L",male,49,1,0,17453,89.1042,C92,C +455,0,3,"Peduzzi, Mr. Joseph",male,,0,0,A/5 2817,8.05,,S +456,1,3,"Jalsevac, Mr. Ivan",male,29,0,0,349240,7.8958,,C +457,0,1,"Millet, Mr. Francis Davis",male,65,0,0,13509,26.55,E38,S +458,1,1,"Kenyon, Mrs. Frederick R (Marion)",female,,1,0,17464,51.8625,D21,S +459,1,2,"Toomey, Miss. Ellen",female,50,0,0,F.C.C. 13531,10.5,,S +460,0,3,"O'Connor, Mr. Maurice",male,,0,0,371060,7.75,,Q +461,1,1,"Anderson, Mr. Harry",male,48,0,0,19952,26.55,E12,S +462,0,3,"Morley, Mr. William",male,34,0,0,364506,8.05,,S +463,0,1,"Gee, Mr. Arthur H",male,47,0,0,111320,38.5,E63,S +464,0,2,"Milling, Mr. Jacob Christian",male,48,0,0,234360,13,,S +465,0,3,"Maisner, Mr. Simon",male,,0,0,A/S 2816,8.05,,S +466,0,3,"Goncalves, Mr. Manuel Estanslas",male,38,0,0,SOTON/O.Q. 3101306,7.05,,S +467,0,2,"Campbell, Mr. William",male,,0,0,239853,0,,S +468,0,1,"Smart, Mr. John Montgomery",male,56,0,0,113792,26.55,,S +469,0,3,"Scanlan, Mr. James",male,,0,0,36209,7.725,,Q +470,1,3,"Baclini, Miss. Helene Barbara",female,0.75,2,1,2666,19.2583,,C +471,0,3,"Keefe, Mr. Arthur",male,,0,0,323592,7.25,,S +472,0,3,"Cacic, Mr. Luka",male,38,0,0,315089,8.6625,,S +473,1,2,"West, Mrs. Edwy Arthur (Ada Mary Worth)",female,33,1,2,C.A. 34651,27.75,,S +474,1,2,"Jerwan, Mrs. Amin S (Marie Marthe Thuillard)",female,23,0,0,SC/AH Basle 541,13.7917,D,C +475,0,3,"Strandberg, Miss. Ida Sofia",female,22,0,0,7553,9.8375,,S +476,0,1,"Clifford, Mr. George Quincy",male,,0,0,110465,52,A14,S +477,0,2,"Renouf, Mr. Peter Henry",male,34,1,0,31027,21,,S +478,0,3,"Braund, Mr. Lewis Richard",male,29,1,0,3460,7.0458,,S +479,0,3,"Karlsson, Mr. Nils August",male,22,0,0,350060,7.5208,,S +480,1,3,"Hirvonen, Miss. Hildur E",female,2,0,1,3101298,12.2875,,S +481,0,3,"Goodwin, Master. Harold Victor",male,9,5,2,CA 2144,46.9,,S +482,0,2,"Frost, Mr. Anthony Wood ""Archie""",male,,0,0,239854,0,,S +483,0,3,"Rouse, Mr. Richard Henry",male,50,0,0,A/5 3594,8.05,,S +484,1,3,"Turkula, Mrs. (Hedwig)",female,63,0,0,4134,9.5875,,S +485,1,1,"Bishop, Mr. Dickinson H",male,25,1,0,11967,91.0792,B49,C +486,0,3,"Lefebre, Miss. Jeannie",female,,3,1,4133,25.4667,,S +487,1,1,"Hoyt, Mrs. Frederick Maxfield (Jane Anne Forby)",female,35,1,0,19943,90,C93,S +488,0,1,"Kent, Mr. Edward Austin",male,58,0,0,11771,29.7,B37,C +489,0,3,"Somerton, Mr. Francis William",male,30,0,0,A.5. 18509,8.05,,S +490,1,3,"Coutts, Master. Eden Leslie ""Neville""",male,9,1,1,C.A. 37671,15.9,,S +491,0,3,"Hagland, Mr. Konrad Mathias Reiersen",male,,1,0,65304,19.9667,,S +492,0,3,"Windelov, Mr. Einar",male,21,0,0,SOTON/OQ 3101317,7.25,,S +493,0,1,"Molson, Mr. Harry Markland",male,55,0,0,113787,30.5,C30,S +494,0,1,"Artagaveytia, Mr. Ramon",male,71,0,0,PC 17609,49.5042,,C +495,0,3,"Stanley, Mr. Edward Roland",male,21,0,0,A/4 45380,8.05,,S +496,0,3,"Yousseff, Mr. Gerious",male,,0,0,2627,14.4583,,C +497,1,1,"Eustis, Miss. Elizabeth Mussey",female,54,1,0,36947,78.2667,D20,C +498,0,3,"Shellard, Mr. Frederick William",male,,0,0,C.A. 6212,15.1,,S +499,0,1,"Allison, Mrs. Hudson J C (Bessie Waldo Daniels)",female,25,1,2,113781,151.55,C22 C26,S +500,0,3,"Svensson, Mr. Olof",male,24,0,0,350035,7.7958,,S +501,0,3,"Calic, Mr. Petar",male,17,0,0,315086,8.6625,,S +502,0,3,"Canavan, Miss. Mary",female,21,0,0,364846,7.75,,Q +503,0,3,"O'Sullivan, Miss. Bridget Mary",female,,0,0,330909,7.6292,,Q +504,0,3,"Laitinen, Miss. Kristina Sofia",female,37,0,0,4135,9.5875,,S +505,1,1,"Maioni, Miss. Roberta",female,16,0,0,110152,86.5,B79,S +506,0,1,"Penasco y Castellana, Mr. Victor de Satode",male,18,1,0,PC 17758,108.9,C65,C +507,1,2,"Quick, Mrs. Frederick Charles (Jane Richards)",female,33,0,2,26360,26,,S +508,1,1,"Bradley, Mr. George (""George Arthur Brayton"")",male,,0,0,111427,26.55,,S +509,0,3,"Olsen, Mr. Henry Margido",male,28,0,0,C 4001,22.525,,S +510,1,3,"Lang, Mr. Fang",male,26,0,0,1601,56.4958,,S +511,1,3,"Daly, Mr. Eugene Patrick",male,29,0,0,382651,7.75,,Q +512,0,3,"Webber, Mr. James",male,,0,0,SOTON/OQ 3101316,8.05,,S +513,1,1,"McGough, Mr. James Robert",male,36,0,0,PC 17473,26.2875,E25,S +514,1,1,"Rothschild, Mrs. Martin (Elizabeth L. Barrett)",female,54,1,0,PC 17603,59.4,,C +515,0,3,"Coleff, Mr. Satio",male,24,0,0,349209,7.4958,,S +516,0,1,"Walker, Mr. William Anderson",male,47,0,0,36967,34.0208,D46,S +517,1,2,"Lemore, Mrs. (Amelia Milley)",female,34,0,0,C.A. 34260,10.5,F33,S +518,0,3,"Ryan, Mr. Patrick",male,,0,0,371110,24.15,,Q +519,1,2,"Angle, Mrs. William A (Florence ""Mary"" Agnes Hughes)",female,36,1,0,226875,26,,S +520,0,3,"Pavlovic, Mr. Stefo",male,32,0,0,349242,7.8958,,S +521,1,1,"Perreault, Miss. Anne",female,30,0,0,12749,93.5,B73,S +522,0,3,"Vovk, Mr. Janko",male,22,0,0,349252,7.8958,,S +523,0,3,"Lahoud, Mr. Sarkis",male,,0,0,2624,7.225,,C +524,1,1,"Hippach, Mrs. Louis Albert (Ida Sophia Fischer)",female,44,0,1,111361,57.9792,B18,C +525,0,3,"Kassem, Mr. Fared",male,,0,0,2700,7.2292,,C +526,0,3,"Farrell, Mr. James",male,40.5,0,0,367232,7.75,,Q +527,1,2,"Ridsdale, Miss. Lucy",female,50,0,0,W./C. 14258,10.5,,S +528,0,1,"Farthing, Mr. John",male,,0,0,PC 17483,221.7792,C95,S +529,0,3,"Salonen, Mr. Johan Werner",male,39,0,0,3101296,7.925,,S +530,0,2,"Hocking, Mr. Richard George",male,23,2,1,29104,11.5,,S +531,1,2,"Quick, Miss. Phyllis May",female,2,1,1,26360,26,,S +532,0,3,"Toufik, Mr. Nakli",male,,0,0,2641,7.2292,,C +533,0,3,"Elias, Mr. Joseph Jr",male,17,1,1,2690,7.2292,,C +534,1,3,"Peter, Mrs. Catherine (Catherine Rizk)",female,,0,2,2668,22.3583,,C +535,0,3,"Cacic, Miss. Marija",female,30,0,0,315084,8.6625,,S +536,1,2,"Hart, Miss. Eva Miriam",female,7,0,2,F.C.C. 13529,26.25,,S +537,0,1,"Butt, Major. Archibald Willingham",male,45,0,0,113050,26.55,B38,S +538,1,1,"LeRoy, Miss. Bertha",female,30,0,0,PC 17761,106.425,,C +539,0,3,"Risien, Mr. Samuel Beard",male,,0,0,364498,14.5,,S +540,1,1,"Frolicher, Miss. Hedwig Margaritha",female,22,0,2,13568,49.5,B39,C +541,1,1,"Crosby, Miss. Harriet R",female,36,0,2,WE/P 5735,71,B22,S +542,0,3,"Andersson, Miss. Ingeborg Constanzia",female,9,4,2,347082,31.275,,S +543,0,3,"Andersson, Miss. Sigrid Elisabeth",female,11,4,2,347082,31.275,,S +544,1,2,"Beane, Mr. Edward",male,32,1,0,2908,26,,S +545,0,1,"Douglas, Mr. Walter Donald",male,50,1,0,PC 17761,106.425,C86,C +546,0,1,"Nicholson, Mr. Arthur Ernest",male,64,0,0,693,26,,S +547,1,2,"Beane, Mrs. Edward (Ethel Clarke)",female,19,1,0,2908,26,,S +548,1,2,"Padro y Manent, Mr. Julian",male,,0,0,SC/PARIS 2146,13.8625,,C +549,0,3,"Goldsmith, Mr. Frank John",male,33,1,1,363291,20.525,,S +550,1,2,"Davies, Master. John Morgan Jr",male,8,1,1,C.A. 33112,36.75,,S +551,1,1,"Thayer, Mr. John Borland Jr",male,17,0,2,17421,110.8833,C70,C +552,0,2,"Sharp, Mr. Percival James R",male,27,0,0,244358,26,,S +553,0,3,"O'Brien, Mr. Timothy",male,,0,0,330979,7.8292,,Q +554,1,3,"Leeni, Mr. Fahim (""Philip Zenni"")",male,22,0,0,2620,7.225,,C +555,1,3,"Ohman, Miss. Velin",female,22,0,0,347085,7.775,,S +556,0,1,"Wright, Mr. George",male,62,0,0,113807,26.55,,S +557,1,1,"Duff Gordon, Lady. (Lucille Christiana Sutherland) (""Mrs Morgan"")",female,48,1,0,11755,39.6,A16,C +558,0,1,"Robbins, Mr. Victor",male,,0,0,PC 17757,227.525,,C +559,1,1,"Taussig, Mrs. Emil (Tillie Mandelbaum)",female,39,1,1,110413,79.65,E67,S +560,1,3,"de Messemaeker, Mrs. Guillaume Joseph (Emma)",female,36,1,0,345572,17.4,,S +561,0,3,"Morrow, Mr. Thomas Rowan",male,,0,0,372622,7.75,,Q +562,0,3,"Sivic, Mr. Husein",male,40,0,0,349251,7.8958,,S +563,0,2,"Norman, Mr. Robert Douglas",male,28,0,0,218629,13.5,,S +564,0,3,"Simmons, Mr. John",male,,0,0,SOTON/OQ 392082,8.05,,S +565,0,3,"Meanwell, Miss. (Marion Ogden)",female,,0,0,SOTON/O.Q. 392087,8.05,,S +566,0,3,"Davies, Mr. Alfred J",male,24,2,0,A/4 48871,24.15,,S +567,0,3,"Stoytcheff, Mr. Ilia",male,19,0,0,349205,7.8958,,S +568,0,3,"Palsson, Mrs. Nils (Alma Cornelia Berglund)",female,29,0,4,349909,21.075,,S +569,0,3,"Doharr, Mr. Tannous",male,,0,0,2686,7.2292,,C +570,1,3,"Jonsson, Mr. Carl",male,32,0,0,350417,7.8542,,S +571,1,2,"Harris, Mr. George",male,62,0,0,S.W./PP 752,10.5,,S +572,1,1,"Appleton, Mrs. Edward Dale (Charlotte Lamson)",female,53,2,0,11769,51.4792,C101,S +573,1,1,"Flynn, Mr. John Irwin (""Irving"")",male,36,0,0,PC 17474,26.3875,E25,S +574,1,3,"Kelly, Miss. Mary",female,,0,0,14312,7.75,,Q +575,0,3,"Rush, Mr. Alfred George John",male,16,0,0,A/4. 20589,8.05,,S +576,0,3,"Patchett, Mr. George",male,19,0,0,358585,14.5,,S +577,1,2,"Garside, Miss. Ethel",female,34,0,0,243880,13,,S +578,1,1,"Silvey, Mrs. William Baird (Alice Munger)",female,39,1,0,13507,55.9,E44,S +579,0,3,"Caram, Mrs. Joseph (Maria Elias)",female,,1,0,2689,14.4583,,C +580,1,3,"Jussila, Mr. Eiriik",male,32,0,0,STON/O 2. 3101286,7.925,,S +581,1,2,"Christy, Miss. Julie Rachel",female,25,1,1,237789,30,,S +582,1,1,"Thayer, Mrs. John Borland (Marian Longstreth Morris)",female,39,1,1,17421,110.8833,C68,C +583,0,2,"Downton, Mr. William James",male,54,0,0,28403,26,,S +584,0,1,"Ross, Mr. John Hugo",male,36,0,0,13049,40.125,A10,C +585,0,3,"Paulner, Mr. Uscher",male,,0,0,3411,8.7125,,C +586,1,1,"Taussig, Miss. Ruth",female,18,0,2,110413,79.65,E68,S +587,0,2,"Jarvis, Mr. John Denzil",male,47,0,0,237565,15,,S +588,1,1,"Frolicher-Stehli, Mr. Maxmillian",male,60,1,1,13567,79.2,B41,C +589,0,3,"Gilinski, Mr. Eliezer",male,22,0,0,14973,8.05,,S +590,0,3,"Murdlin, Mr. Joseph",male,,0,0,A./5. 3235,8.05,,S +591,0,3,"Rintamaki, Mr. Matti",male,35,0,0,STON/O 2. 3101273,7.125,,S +592,1,1,"Stephenson, Mrs. Walter Bertram (Martha Eustis)",female,52,1,0,36947,78.2667,D20,C +593,0,3,"Elsbury, Mr. William James",male,47,0,0,A/5 3902,7.25,,S +594,0,3,"Bourke, Miss. Mary",female,,0,2,364848,7.75,,Q +595,0,2,"Chapman, Mr. John Henry",male,37,1,0,SC/AH 29037,26,,S +596,0,3,"Van Impe, Mr. Jean Baptiste",male,36,1,1,345773,24.15,,S +597,1,2,"Leitch, Miss. Jessie Wills",female,,0,0,248727,33,,S +598,0,3,"Johnson, Mr. Alfred",male,49,0,0,LINE,0,,S +599,0,3,"Boulos, Mr. Hanna",male,,0,0,2664,7.225,,C +600,1,1,"Duff Gordon, Sir. Cosmo Edmund (""Mr Morgan"")",male,49,1,0,PC 17485,56.9292,A20,C +601,1,2,"Jacobsohn, Mrs. Sidney Samuel (Amy Frances Christy)",female,24,2,1,243847,27,,S +602,0,3,"Slabenoff, Mr. Petco",male,,0,0,349214,7.8958,,S +603,0,1,"Harrington, Mr. Charles H",male,,0,0,113796,42.4,,S +604,0,3,"Torber, Mr. Ernst William",male,44,0,0,364511,8.05,,S +605,1,1,"Homer, Mr. Harry (""Mr E Haven"")",male,35,0,0,111426,26.55,,C +606,0,3,"Lindell, Mr. Edvard Bengtsson",male,36,1,0,349910,15.55,,S +607,0,3,"Karaic, Mr. Milan",male,30,0,0,349246,7.8958,,S +608,1,1,"Daniel, Mr. Robert Williams",male,27,0,0,113804,30.5,,S +609,1,2,"Laroche, Mrs. Joseph (Juliette Marie Louise Lafargue)",female,22,1,2,SC/Paris 2123,41.5792,,C +610,1,1,"Shutes, Miss. Elizabeth W",female,40,0,0,PC 17582,153.4625,C125,S +611,0,3,"Andersson, Mrs. Anders Johan (Alfrida Konstantia Brogren)",female,39,1,5,347082,31.275,,S +612,0,3,"Jardin, Mr. Jose Neto",male,,0,0,SOTON/O.Q. 3101305,7.05,,S +613,1,3,"Murphy, Miss. Margaret Jane",female,,1,0,367230,15.5,,Q +614,0,3,"Horgan, Mr. John",male,,0,0,370377,7.75,,Q +615,0,3,"Brocklebank, Mr. William Alfred",male,35,0,0,364512,8.05,,S +616,1,2,"Herman, Miss. Alice",female,24,1,2,220845,65,,S +617,0,3,"Danbom, Mr. Ernst Gilbert",male,34,1,1,347080,14.4,,S +618,0,3,"Lobb, Mrs. William Arthur (Cordelia K Stanlick)",female,26,1,0,A/5. 3336,16.1,,S +619,1,2,"Becker, Miss. Marion Louise",female,4,2,1,230136,39,F4,S +620,0,2,"Gavey, Mr. Lawrence",male,26,0,0,31028,10.5,,S +621,0,3,"Yasbeck, Mr. Antoni",male,27,1,0,2659,14.4542,,C +622,1,1,"Kimball, Mr. Edwin Nelson Jr",male,42,1,0,11753,52.5542,D19,S +623,1,3,"Nakid, Mr. Sahid",male,20,1,1,2653,15.7417,,C +624,0,3,"Hansen, Mr. Henry Damsgaard",male,21,0,0,350029,7.8542,,S +625,0,3,"Bowen, Mr. David John ""Dai""",male,21,0,0,54636,16.1,,S +626,0,1,"Sutton, Mr. Frederick",male,61,0,0,36963,32.3208,D50,S +627,0,2,"Kirkland, Rev. Charles Leonard",male,57,0,0,219533,12.35,,Q +628,1,1,"Longley, Miss. Gretchen Fiske",female,21,0,0,13502,77.9583,D9,S +629,0,3,"Bostandyeff, Mr. Guentcho",male,26,0,0,349224,7.8958,,S +630,0,3,"O'Connell, Mr. Patrick D",male,,0,0,334912,7.7333,,Q +631,1,1,"Barkworth, Mr. Algernon Henry Wilson",male,80,0,0,27042,30,A23,S +632,0,3,"Lundahl, Mr. Johan Svensson",male,51,0,0,347743,7.0542,,S +633,1,1,"Stahelin-Maeglin, Dr. Max",male,32,0,0,13214,30.5,B50,C +634,0,1,"Parr, Mr. William Henry Marsh",male,,0,0,112052,0,,S +635,0,3,"Skoog, Miss. Mabel",female,9,3,2,347088,27.9,,S +636,1,2,"Davis, Miss. Mary",female,28,0,0,237668,13,,S +637,0,3,"Leinonen, Mr. Antti Gustaf",male,32,0,0,STON/O 2. 3101292,7.925,,S +638,0,2,"Collyer, Mr. Harvey",male,31,1,1,C.A. 31921,26.25,,S +639,0,3,"Panula, Mrs. Juha (Maria Emilia Ojala)",female,41,0,5,3101295,39.6875,,S +640,0,3,"Thorneycroft, Mr. Percival",male,,1,0,376564,16.1,,S +641,0,3,"Jensen, Mr. Hans Peder",male,20,0,0,350050,7.8542,,S +642,1,1,"Sagesser, Mlle. Emma",female,24,0,0,PC 17477,69.3,B35,C +643,0,3,"Skoog, Miss. Margit Elizabeth",female,2,3,2,347088,27.9,,S +644,1,3,"Foo, Mr. Choong",male,,0,0,1601,56.4958,,S +645,1,3,"Baclini, Miss. Eugenie",female,0.75,2,1,2666,19.2583,,C +646,1,1,"Harper, Mr. Henry Sleeper",male,48,1,0,PC 17572,76.7292,D33,C +647,0,3,"Cor, Mr. Liudevit",male,19,0,0,349231,7.8958,,S +648,1,1,"Simonius-Blumer, Col. Oberst Alfons",male,56,0,0,13213,35.5,A26,C +649,0,3,"Willey, Mr. Edward",male,,0,0,S.O./P.P. 751,7.55,,S +650,1,3,"Stanley, Miss. Amy Zillah Elsie",female,23,0,0,CA. 2314,7.55,,S +651,0,3,"Mitkoff, Mr. Mito",male,,0,0,349221,7.8958,,S +652,1,2,"Doling, Miss. Elsie",female,18,0,1,231919,23,,S +653,0,3,"Kalvik, Mr. Johannes Halvorsen",male,21,0,0,8475,8.4333,,S +654,1,3,"O'Leary, Miss. Hanora ""Norah""",female,,0,0,330919,7.8292,,Q +655,0,3,"Hegarty, Miss. Hanora ""Nora""",female,18,0,0,365226,6.75,,Q +656,0,2,"Hickman, Mr. Leonard Mark",male,24,2,0,S.O.C. 14879,73.5,,S +657,0,3,"Radeff, Mr. Alexander",male,,0,0,349223,7.8958,,S +658,0,3,"Bourke, Mrs. John (Catherine)",female,32,1,1,364849,15.5,,Q +659,0,2,"Eitemiller, Mr. George Floyd",male,23,0,0,29751,13,,S +660,0,1,"Newell, Mr. Arthur Webster",male,58,0,2,35273,113.275,D48,C +661,1,1,"Frauenthal, Dr. Henry William",male,50,2,0,PC 17611,133.65,,S +662,0,3,"Badt, Mr. Mohamed",male,40,0,0,2623,7.225,,C +663,0,1,"Colley, Mr. Edward Pomeroy",male,47,0,0,5727,25.5875,E58,S +664,0,3,"Coleff, Mr. Peju",male,36,0,0,349210,7.4958,,S +665,1,3,"Lindqvist, Mr. Eino William",male,20,1,0,STON/O 2. 3101285,7.925,,S +666,0,2,"Hickman, Mr. Lewis",male,32,2,0,S.O.C. 14879,73.5,,S +667,0,2,"Butler, Mr. Reginald Fenton",male,25,0,0,234686,13,,S +668,0,3,"Rommetvedt, Mr. Knud Paust",male,,0,0,312993,7.775,,S +669,0,3,"Cook, Mr. Jacob",male,43,0,0,A/5 3536,8.05,,S +670,1,1,"Taylor, Mrs. Elmer Zebley (Juliet Cummins Wright)",female,,1,0,19996,52,C126,S +671,1,2,"Brown, Mrs. Thomas William Solomon (Elizabeth Catherine Ford)",female,40,1,1,29750,39,,S +672,0,1,"Davidson, Mr. Thornton",male,31,1,0,F.C. 12750,52,B71,S +673,0,2,"Mitchell, Mr. Henry Michael",male,70,0,0,C.A. 24580,10.5,,S +674,1,2,"Wilhelms, Mr. Charles",male,31,0,0,244270,13,,S +675,0,2,"Watson, Mr. Ennis Hastings",male,,0,0,239856,0,,S +676,0,3,"Edvardsson, Mr. Gustaf Hjalmar",male,18,0,0,349912,7.775,,S +677,0,3,"Sawyer, Mr. Frederick Charles",male,24.5,0,0,342826,8.05,,S +678,1,3,"Turja, Miss. Anna Sofia",female,18,0,0,4138,9.8417,,S +679,0,3,"Goodwin, Mrs. Frederick (Augusta Tyler)",female,43,1,6,CA 2144,46.9,,S +680,1,1,"Cardeza, Mr. Thomas Drake Martinez",male,36,0,1,PC 17755,512.3292,B51 B53 B55,C +681,0,3,"Peters, Miss. Katie",female,,0,0,330935,8.1375,,Q +682,1,1,"Hassab, Mr. Hammad",male,27,0,0,PC 17572,76.7292,D49,C +683,0,3,"Olsvigen, Mr. Thor Anderson",male,20,0,0,6563,9.225,,S +684,0,3,"Goodwin, Mr. Charles Edward",male,14,5,2,CA 2144,46.9,,S +685,0,2,"Brown, Mr. Thomas William Solomon",male,60,1,1,29750,39,,S +686,0,2,"Laroche, Mr. Joseph Philippe Lemercier",male,25,1,2,SC/Paris 2123,41.5792,,C +687,0,3,"Panula, Mr. Jaako Arnold",male,14,4,1,3101295,39.6875,,S +688,0,3,"Dakic, Mr. Branko",male,19,0,0,349228,10.1708,,S +689,0,3,"Fischer, Mr. Eberhard Thelander",male,18,0,0,350036,7.7958,,S +690,1,1,"Madill, Miss. Georgette Alexandra",female,15,0,1,24160,211.3375,B5,S +691,1,1,"Dick, Mr. Albert Adrian",male,31,1,0,17474,57,B20,S +692,1,3,"Karun, Miss. Manca",female,4,0,1,349256,13.4167,,C +693,1,3,"Lam, Mr. Ali",male,,0,0,1601,56.4958,,S +694,0,3,"Saad, Mr. Khalil",male,25,0,0,2672,7.225,,C +695,0,1,"Weir, Col. John",male,60,0,0,113800,26.55,,S +696,0,2,"Chapman, Mr. Charles Henry",male,52,0,0,248731,13.5,,S +697,0,3,"Kelly, Mr. James",male,44,0,0,363592,8.05,,S +698,1,3,"Mullens, Miss. Katherine ""Katie""",female,,0,0,35852,7.7333,,Q +699,0,1,"Thayer, Mr. John Borland",male,49,1,1,17421,110.8833,C68,C +700,0,3,"Humblen, Mr. Adolf Mathias Nicolai Olsen",male,42,0,0,348121,7.65,F G63,S +701,1,1,"Astor, Mrs. John Jacob (Madeleine Talmadge Force)",female,18,1,0,PC 17757,227.525,C62 C64,C +702,1,1,"Silverthorne, Mr. Spencer Victor",male,35,0,0,PC 17475,26.2875,E24,S +703,0,3,"Barbara, Miss. Saiide",female,18,0,1,2691,14.4542,,C +704,0,3,"Gallagher, Mr. Martin",male,25,0,0,36864,7.7417,,Q +705,0,3,"Hansen, Mr. Henrik Juul",male,26,1,0,350025,7.8542,,S +706,0,2,"Morley, Mr. Henry Samuel (""Mr Henry Marshall"")",male,39,0,0,250655,26,,S +707,1,2,"Kelly, Mrs. Florence ""Fannie""",female,45,0,0,223596,13.5,,S +708,1,1,"Calderhead, Mr. Edward Pennington",male,42,0,0,PC 17476,26.2875,E24,S +709,1,1,"Cleaver, Miss. Alice",female,22,0,0,113781,151.55,,S +710,1,3,"Moubarek, Master. Halim Gonios (""William George"")",male,,1,1,2661,15.2458,,C +711,1,1,"Mayne, Mlle. Berthe Antonine (""Mrs de Villiers"")",female,24,0,0,PC 17482,49.5042,C90,C +712,0,1,"Klaber, Mr. Herman",male,,0,0,113028,26.55,C124,S +713,1,1,"Taylor, Mr. Elmer Zebley",male,48,1,0,19996,52,C126,S +714,0,3,"Larsson, Mr. August Viktor",male,29,0,0,7545,9.4833,,S +715,0,2,"Greenberg, Mr. Samuel",male,52,0,0,250647,13,,S +716,0,3,"Soholt, Mr. Peter Andreas Lauritz Andersen",male,19,0,0,348124,7.65,F G73,S +717,1,1,"Endres, Miss. Caroline Louise",female,38,0,0,PC 17757,227.525,C45,C +718,1,2,"Troutt, Miss. Edwina Celia ""Winnie""",female,27,0,0,34218,10.5,E101,S +719,0,3,"McEvoy, Mr. Michael",male,,0,0,36568,15.5,,Q +720,0,3,"Johnson, Mr. Malkolm Joackim",male,33,0,0,347062,7.775,,S +721,1,2,"Harper, Miss. Annie Jessie ""Nina""",female,6,0,1,248727,33,,S +722,0,3,"Jensen, Mr. Svend Lauritz",male,17,1,0,350048,7.0542,,S +723,0,2,"Gillespie, Mr. William Henry",male,34,0,0,12233,13,,S +724,0,2,"Hodges, Mr. Henry Price",male,50,0,0,250643,13,,S +725,1,1,"Chambers, Mr. Norman Campbell",male,27,1,0,113806,53.1,E8,S +726,0,3,"Oreskovic, Mr. Luka",male,20,0,0,315094,8.6625,,S +727,1,2,"Renouf, Mrs. Peter Henry (Lillian Jefferys)",female,30,3,0,31027,21,,S +728,1,3,"Mannion, Miss. Margareth",female,,0,0,36866,7.7375,,Q +729,0,2,"Bryhl, Mr. Kurt Arnold Gottfrid",male,25,1,0,236853,26,,S +730,0,3,"Ilmakangas, Miss. Pieta Sofia",female,25,1,0,STON/O2. 3101271,7.925,,S +731,1,1,"Allen, Miss. Elisabeth Walton",female,29,0,0,24160,211.3375,B5,S +732,0,3,"Hassan, Mr. Houssein G N",male,11,0,0,2699,18.7875,,C +733,0,2,"Knight, Mr. Robert J",male,,0,0,239855,0,,S +734,0,2,"Berriman, Mr. William John",male,23,0,0,28425,13,,S +735,0,2,"Troupiansky, Mr. Moses Aaron",male,23,0,0,233639,13,,S +736,0,3,"Williams, Mr. Leslie",male,28.5,0,0,54636,16.1,,S +737,0,3,"Ford, Mrs. Edward (Margaret Ann Watson)",female,48,1,3,W./C. 6608,34.375,,S +738,1,1,"Lesurer, Mr. Gustave J",male,35,0,0,PC 17755,512.3292,B101,C +739,0,3,"Ivanoff, Mr. Kanio",male,,0,0,349201,7.8958,,S +740,0,3,"Nankoff, Mr. Minko",male,,0,0,349218,7.8958,,S +741,1,1,"Hawksford, Mr. Walter James",male,,0,0,16988,30,D45,S +742,0,1,"Cavendish, Mr. Tyrell William",male,36,1,0,19877,78.85,C46,S +743,1,1,"Ryerson, Miss. Susan Parker ""Suzette""",female,21,2,2,PC 17608,262.375,B57 B59 B63 B66,C +744,0,3,"McNamee, Mr. Neal",male,24,1,0,376566,16.1,,S +745,1,3,"Stranden, Mr. Juho",male,31,0,0,STON/O 2. 3101288,7.925,,S +746,0,1,"Crosby, Capt. Edward Gifford",male,70,1,1,WE/P 5735,71,B22,S +747,0,3,"Abbott, Mr. Rossmore Edward",male,16,1,1,C.A. 2673,20.25,,S +748,1,2,"Sinkkonen, Miss. Anna",female,30,0,0,250648,13,,S +749,0,1,"Marvin, Mr. Daniel Warner",male,19,1,0,113773,53.1,D30,S +750,0,3,"Connaghton, Mr. Michael",male,31,0,0,335097,7.75,,Q +751,1,2,"Wells, Miss. Joan",female,4,1,1,29103,23,,S +752,1,3,"Moor, Master. Meier",male,6,0,1,392096,12.475,E121,S +753,0,3,"Vande Velde, Mr. Johannes Joseph",male,33,0,0,345780,9.5,,S +754,0,3,"Jonkoff, Mr. Lalio",male,23,0,0,349204,7.8958,,S +755,1,2,"Herman, Mrs. Samuel (Jane Laver)",female,48,1,2,220845,65,,S +756,1,2,"Hamalainen, Master. Viljo",male,0.67,1,1,250649,14.5,,S +757,0,3,"Carlsson, Mr. August Sigfrid",male,28,0,0,350042,7.7958,,S +758,0,2,"Bailey, Mr. Percy Andrew",male,18,0,0,29108,11.5,,S +759,0,3,"Theobald, Mr. Thomas Leonard",male,34,0,0,363294,8.05,,S +760,1,1,"Rothes, the Countess. of (Lucy Noel Martha Dyer-Edwards)",female,33,0,0,110152,86.5,B77,S +761,0,3,"Garfirth, Mr. John",male,,0,0,358585,14.5,,S +762,0,3,"Nirva, Mr. Iisakki Antino Aijo",male,41,0,0,SOTON/O2 3101272,7.125,,S +763,1,3,"Barah, Mr. Hanna Assi",male,20,0,0,2663,7.2292,,C +764,1,1,"Carter, Mrs. William Ernest (Lucile Polk)",female,36,1,2,113760,120,B96 B98,S +765,0,3,"Eklund, Mr. Hans Linus",male,16,0,0,347074,7.775,,S +766,1,1,"Hogeboom, Mrs. John C (Anna Andrews)",female,51,1,0,13502,77.9583,D11,S +767,0,1,"Brewe, Dr. Arthur Jackson",male,,0,0,112379,39.6,,C +768,0,3,"Mangan, Miss. Mary",female,30.5,0,0,364850,7.75,,Q +769,0,3,"Moran, Mr. Daniel J",male,,1,0,371110,24.15,,Q +770,0,3,"Gronnestad, Mr. Daniel Danielsen",male,32,0,0,8471,8.3625,,S +771,0,3,"Lievens, Mr. Rene Aime",male,24,0,0,345781,9.5,,S +772,0,3,"Jensen, Mr. Niels Peder",male,48,0,0,350047,7.8542,,S +773,0,2,"Mack, Mrs. (Mary)",female,57,0,0,S.O./P.P. 3,10.5,E77,S +774,0,3,"Elias, Mr. Dibo",male,,0,0,2674,7.225,,C +775,1,2,"Hocking, Mrs. Elizabeth (Eliza Needs)",female,54,1,3,29105,23,,S +776,0,3,"Myhrman, Mr. Pehr Fabian Oliver Malkolm",male,18,0,0,347078,7.75,,S +777,0,3,"Tobin, Mr. Roger",male,,0,0,383121,7.75,F38,Q +778,1,3,"Emanuel, Miss. Virginia Ethel",female,5,0,0,364516,12.475,,S +779,0,3,"Kilgannon, Mr. Thomas J",male,,0,0,36865,7.7375,,Q +780,1,1,"Robert, Mrs. Edward Scott (Elisabeth Walton McMillan)",female,43,0,1,24160,211.3375,B3,S +781,1,3,"Ayoub, Miss. Banoura",female,13,0,0,2687,7.2292,,C +782,1,1,"Dick, Mrs. Albert Adrian (Vera Gillespie)",female,17,1,0,17474,57,B20,S +783,0,1,"Long, Mr. Milton Clyde",male,29,0,0,113501,30,D6,S +784,0,3,"Johnston, Mr. Andrew G",male,,1,2,W./C. 6607,23.45,,S +785,0,3,"Ali, Mr. William",male,25,0,0,SOTON/O.Q. 3101312,7.05,,S +786,0,3,"Harmer, Mr. Abraham (David Lishin)",male,25,0,0,374887,7.25,,S +787,1,3,"Sjoblom, Miss. Anna Sofia",female,18,0,0,3101265,7.4958,,S +788,0,3,"Rice, Master. George Hugh",male,8,4,1,382652,29.125,,Q +789,1,3,"Dean, Master. Bertram Vere",male,1,1,2,C.A. 2315,20.575,,S +790,0,1,"Guggenheim, Mr. Benjamin",male,46,0,0,PC 17593,79.2,B82 B84,C +791,0,3,"Keane, Mr. Andrew ""Andy""",male,,0,0,12460,7.75,,Q +792,0,2,"Gaskell, Mr. Alfred",male,16,0,0,239865,26,,S +793,0,3,"Sage, Miss. Stella Anna",female,,8,2,CA. 2343,69.55,,S +794,0,1,"Hoyt, Mr. William Fisher",male,,0,0,PC 17600,30.6958,,C +795,0,3,"Dantcheff, Mr. Ristiu",male,25,0,0,349203,7.8958,,S +796,0,2,"Otter, Mr. Richard",male,39,0,0,28213,13,,S +797,1,1,"Leader, Dr. Alice (Farnham)",female,49,0,0,17465,25.9292,D17,S +798,1,3,"Osman, Mrs. Mara",female,31,0,0,349244,8.6833,,S +799,0,3,"Ibrahim Shawah, Mr. Yousseff",male,30,0,0,2685,7.2292,,C +800,0,3,"Van Impe, Mrs. Jean Baptiste (Rosalie Paula Govaert)",female,30,1,1,345773,24.15,,S +801,0,2,"Ponesell, Mr. Martin",male,34,0,0,250647,13,,S +802,1,2,"Collyer, Mrs. Harvey (Charlotte Annie Tate)",female,31,1,1,C.A. 31921,26.25,,S +803,1,1,"Carter, Master. William Thornton II",male,11,1,2,113760,120,B96 B98,S +804,1,3,"Thomas, Master. Assad Alexander",male,0.42,0,1,2625,8.5167,,C +805,1,3,"Hedman, Mr. Oskar Arvid",male,27,0,0,347089,6.975,,S +806,0,3,"Johansson, Mr. Karl Johan",male,31,0,0,347063,7.775,,S +807,0,1,"Andrews, Mr. Thomas Jr",male,39,0,0,112050,0,A36,S +808,0,3,"Pettersson, Miss. Ellen Natalia",female,18,0,0,347087,7.775,,S +809,0,2,"Meyer, Mr. August",male,39,0,0,248723,13,,S +810,1,1,"Chambers, Mrs. Norman Campbell (Bertha Griggs)",female,33,1,0,113806,53.1,E8,S +811,0,3,"Alexander, Mr. William",male,26,0,0,3474,7.8875,,S +812,0,3,"Lester, Mr. James",male,39,0,0,A/4 48871,24.15,,S +813,0,2,"Slemen, Mr. Richard James",male,35,0,0,28206,10.5,,S +814,0,3,"Andersson, Miss. Ebba Iris Alfrida",female,6,4,2,347082,31.275,,S +815,0,3,"Tomlin, Mr. Ernest Portage",male,30.5,0,0,364499,8.05,,S +816,0,1,"Fry, Mr. Richard",male,,0,0,112058,0,B102,S +817,0,3,"Heininen, Miss. Wendla Maria",female,23,0,0,STON/O2. 3101290,7.925,,S +818,0,2,"Mallet, Mr. Albert",male,31,1,1,S.C./PARIS 2079,37.0042,,C +819,0,3,"Holm, Mr. John Fredrik Alexander",male,43,0,0,C 7075,6.45,,S +820,0,3,"Skoog, Master. Karl Thorsten",male,10,3,2,347088,27.9,,S +821,1,1,"Hays, Mrs. Charles Melville (Clara Jennings Gregg)",female,52,1,1,12749,93.5,B69,S +822,1,3,"Lulic, Mr. Nikola",male,27,0,0,315098,8.6625,,S +823,0,1,"Reuchlin, Jonkheer. John George",male,38,0,0,19972,0,,S +824,1,3,"Moor, Mrs. (Beila)",female,27,0,1,392096,12.475,E121,S +825,0,3,"Panula, Master. Urho Abraham",male,2,4,1,3101295,39.6875,,S +826,0,3,"Flynn, Mr. John",male,,0,0,368323,6.95,,Q +827,0,3,"Lam, Mr. Len",male,,0,0,1601,56.4958,,S +828,1,2,"Mallet, Master. Andre",male,1,0,2,S.C./PARIS 2079,37.0042,,C +829,1,3,"McCormack, Mr. Thomas Joseph",male,,0,0,367228,7.75,,Q +830,1,1,"Stone, Mrs. George Nelson (Martha Evelyn)",female,62,0,0,113572,80,B28, +831,1,3,"Yasbeck, Mrs. Antoni (Selini Alexander)",female,15,1,0,2659,14.4542,,C +832,1,2,"Richards, Master. George Sibley",male,0.83,1,1,29106,18.75,,S +833,0,3,"Saad, Mr. Amin",male,,0,0,2671,7.2292,,C +834,0,3,"Augustsson, Mr. Albert",male,23,0,0,347468,7.8542,,S +835,0,3,"Allum, Mr. Owen George",male,18,0,0,2223,8.3,,S +836,1,1,"Compton, Miss. Sara Rebecca",female,39,1,1,PC 17756,83.1583,E49,C +837,0,3,"Pasic, Mr. Jakob",male,21,0,0,315097,8.6625,,S +838,0,3,"Sirota, Mr. Maurice",male,,0,0,392092,8.05,,S +839,1,3,"Chip, Mr. Chang",male,32,0,0,1601,56.4958,,S +840,1,1,"Marechal, Mr. Pierre",male,,0,0,11774,29.7,C47,C +841,0,3,"Alhomaki, Mr. Ilmari Rudolf",male,20,0,0,SOTON/O2 3101287,7.925,,S +842,0,2,"Mudd, Mr. Thomas Charles",male,16,0,0,S.O./P.P. 3,10.5,,S +843,1,1,"Serepeca, Miss. Augusta",female,30,0,0,113798,31,,C +844,0,3,"Lemberopolous, Mr. Peter L",male,34.5,0,0,2683,6.4375,,C +845,0,3,"Culumovic, Mr. Jeso",male,17,0,0,315090,8.6625,,S +846,0,3,"Abbing, Mr. Anthony",male,42,0,0,C.A. 5547,7.55,,S +847,0,3,"Sage, Mr. Douglas Bullen",male,,8,2,CA. 2343,69.55,,S +848,0,3,"Markoff, Mr. Marin",male,35,0,0,349213,7.8958,,C +849,0,2,"Harper, Rev. John",male,28,0,1,248727,33,,S +850,1,1,"Goldenberg, Mrs. Samuel L (Edwiga Grabowska)",female,,1,0,17453,89.1042,C92,C +851,0,3,"Andersson, Master. Sigvard Harald Elias",male,4,4,2,347082,31.275,,S +852,0,3,"Svensson, Mr. Johan",male,74,0,0,347060,7.775,,S +853,0,3,"Boulos, Miss. Nourelain",female,9,1,1,2678,15.2458,,C +854,1,1,"Lines, Miss. Mary Conover",female,16,0,1,PC 17592,39.4,D28,S +855,0,2,"Carter, Mrs. Ernest Courtenay (Lilian Hughes)",female,44,1,0,244252,26,,S +856,1,3,"Aks, Mrs. Sam (Leah Rosen)",female,18,0,1,392091,9.35,,S +857,1,1,"Wick, Mrs. George Dennick (Mary Hitchcock)",female,45,1,1,36928,164.8667,,S +858,1,1,"Daly, Mr. Peter Denis ",male,51,0,0,113055,26.55,E17,S +859,1,3,"Baclini, Mrs. Solomon (Latifa Qurban)",female,24,0,3,2666,19.2583,,C +860,0,3,"Razi, Mr. Raihed",male,,0,0,2629,7.2292,,C +861,0,3,"Hansen, Mr. Claus Peter",male,41,2,0,350026,14.1083,,S +862,0,2,"Giles, Mr. Frederick Edward",male,21,1,0,28134,11.5,,S +863,1,1,"Swift, Mrs. Frederick Joel (Margaret Welles Barron)",female,48,0,0,17466,25.9292,D17,S +864,0,3,"Sage, Miss. Dorothy Edith ""Dolly""",female,,8,2,CA. 2343,69.55,,S +865,0,2,"Gill, Mr. John William",male,24,0,0,233866,13,,S +866,1,2,"Bystrom, Mrs. (Karolina)",female,42,0,0,236852,13,,S +867,1,2,"Duran y More, Miss. Asuncion",female,27,1,0,SC/PARIS 2149,13.8583,,C +868,0,1,"Roebling, Mr. Washington Augustus II",male,31,0,0,PC 17590,50.4958,A24,S +869,0,3,"van Melkebeke, Mr. Philemon",male,,0,0,345777,9.5,,S +870,1,3,"Johnson, Master. Harold Theodor",male,4,1,1,347742,11.1333,,S +871,0,3,"Balkic, Mr. Cerin",male,26,0,0,349248,7.8958,,S +872,1,1,"Beckwith, Mrs. Richard Leonard (Sallie Monypeny)",female,47,1,1,11751,52.5542,D35,S +873,0,1,"Carlsson, Mr. Frans Olof",male,33,0,0,695,5,B51 B53 B55,S +874,0,3,"Vander Cruyssen, Mr. Victor",male,47,0,0,345765,9,,S +875,1,2,"Abelson, Mrs. Samuel (Hannah Wizosky)",female,28,1,0,P/PP 3381,24,,C +876,1,3,"Najib, Miss. Adele Kiamie ""Jane""",female,15,0,0,2667,7.225,,C +877,0,3,"Gustafsson, Mr. Alfred Ossian",male,20,0,0,7534,9.8458,,S +878,0,3,"Petroff, Mr. Nedelio",male,19,0,0,349212,7.8958,,S +879,0,3,"Laleff, Mr. Kristo",male,,0,0,349217,7.8958,,S +880,1,1,"Potter, Mrs. Thomas Jr (Lily Alexenia Wilson)",female,56,0,1,11767,83.1583,C50,C +881,1,2,"Shelley, Mrs. William (Imanita Parrish Hall)",female,25,0,1,230433,26,,S +882,0,3,"Markun, Mr. Johann",male,33,0,0,349257,7.8958,,S +883,0,3,"Dahlberg, Miss. Gerda Ulrika",female,22,0,0,7552,10.5167,,S +884,0,2,"Banfield, Mr. Frederick James",male,28,0,0,C.A./SOTON 34068,10.5,,S +885,0,3,"Sutehall, Mr. Henry Jr",male,25,0,0,SOTON/OQ 392076,7.05,,S +886,0,3,"Rice, Mrs. William (Margaret Norton)",female,39,0,5,382652,29.125,,Q +887,0,2,"Montvila, Rev. Juozas",male,27,0,0,211536,13,,S +888,1,1,"Graham, Miss. Margaret Edith",female,19,0,0,112053,30,B42,S +889,0,3,"Johnston, Miss. Catherine Helen ""Carrie""",female,,1,2,W./C. 6607,23.45,,S +890,1,1,"Behr, Mr. Karl Howell",male,26,0,0,111369,30,C148,C +891,0,3,"Dooley, Mr. Patrick",male,32,0,0,370376,7.75,,Q diff --git a/doc/redirects.csv b/doc/redirects.csv index 3a990b09e7f7d..ef93955c14fe6 100644 --- a/doc/redirects.csv +++ b/doc/redirects.csv @@ -271,21 +271,21 @@ generated/pandas.core.window.Expanding.skew,../reference/api/pandas.core.window. generated/pandas.core.window.Expanding.std,../reference/api/pandas.core.window.Expanding.std generated/pandas.core.window.Expanding.sum,../reference/api/pandas.core.window.Expanding.sum generated/pandas.core.window.Expanding.var,../reference/api/pandas.core.window.Expanding.var -generated/pandas.core.window.Rolling.aggregate,../reference/api/pandas.core.window.Rolling.aggregate -generated/pandas.core.window.Rolling.apply,../reference/api/pandas.core.window.Rolling.apply -generated/pandas.core.window.Rolling.corr,../reference/api/pandas.core.window.Rolling.corr -generated/pandas.core.window.Rolling.count,../reference/api/pandas.core.window.Rolling.count -generated/pandas.core.window.Rolling.cov,../reference/api/pandas.core.window.Rolling.cov -generated/pandas.core.window.Rolling.kurt,../reference/api/pandas.core.window.Rolling.kurt -generated/pandas.core.window.Rolling.max,../reference/api/pandas.core.window.Rolling.max -generated/pandas.core.window.Rolling.mean,../reference/api/pandas.core.window.Rolling.mean -generated/pandas.core.window.Rolling.median,../reference/api/pandas.core.window.Rolling.median -generated/pandas.core.window.Rolling.min,../reference/api/pandas.core.window.Rolling.min -generated/pandas.core.window.Rolling.quantile,../reference/api/pandas.core.window.Rolling.quantile -generated/pandas.core.window.Rolling.skew,../reference/api/pandas.core.window.Rolling.skew -generated/pandas.core.window.Rolling.std,../reference/api/pandas.core.window.Rolling.std -generated/pandas.core.window.Rolling.sum,../reference/api/pandas.core.window.Rolling.sum -generated/pandas.core.window.Rolling.var,../reference/api/pandas.core.window.Rolling.var +generated/pandas.core.window.Rolling.aggregate,../reference/api/pandas.core.window.rolling.Rolling.aggregate +generated/pandas.core.window.Rolling.apply,../reference/api/pandas.core.window.rolling.Rolling.apply +generated/pandas.core.window.Rolling.corr,../reference/api/pandas.core.window.rolling.Rolling.corr +generated/pandas.core.window.Rolling.count,../reference/api/pandas.core.window.rolling.Rolling.count +generated/pandas.core.window.Rolling.cov,../reference/api/pandas.core.window.rolling.Rolling.cov +generated/pandas.core.window.Rolling.kurt,../reference/api/pandas.core.window.rolling.Rolling.kurt +generated/pandas.core.window.Rolling.max,../reference/api/pandas.core.window.rolling.Rolling.max +generated/pandas.core.window.Rolling.mean,../reference/api/pandas.core.window.rolling.Rolling.mean +generated/pandas.core.window.Rolling.median,../reference/api/pandas.core.window.rolling.Rolling.median +generated/pandas.core.window.Rolling.min,../reference/api/pandas.core.window.rolling.Rolling.min +generated/pandas.core.window.Rolling.quantile,../reference/api/pandas.core.window.rolling.Rolling.quantile +generated/pandas.core.window.Rolling.skew,../reference/api/pandas.core.window.rolling.Rolling.skew +generated/pandas.core.window.Rolling.std,../reference/api/pandas.core.window.rolling.Rolling.std +generated/pandas.core.window.Rolling.sum,../reference/api/pandas.core.window.rolling.Rolling.sum +generated/pandas.core.window.Rolling.var,../reference/api/pandas.core.window.rolling.Rolling.var generated/pandas.core.window.Window.mean,../reference/api/pandas.core.window.Window.mean generated/pandas.core.window.Window.sum,../reference/api/pandas.core.window.Window.sum generated/pandas.crosstab,../reference/api/pandas.crosstab diff --git a/doc/source/_static/css/getting_started.css b/doc/source/_static/css/getting_started.css new file mode 100644 index 0000000000000..bb24761cdb159 --- /dev/null +++ b/doc/source/_static/css/getting_started.css @@ -0,0 +1,251 @@ +/* Getting started pages */ + +/* data intro */ +.gs-data { + font-size: 0.9rem; +} + +.gs-data-title { + align-items: center; + font-size: 0.9rem; +} + +.gs-data-title .badge { + margin: 10px; + padding: 5px; +} + +.gs-data .badge { + cursor: pointer; + padding: 10px; + border: none; + text-align: left; + outline: none; + font-size: 12px; +} + +.gs-data .btn { + background-color: grey; + border: none; +} + +/* note/alert properties */ + +.alert-heading { + font-size: 1.2rem; +} + +/* callout properties */ +.gs-callout { + padding: 20px; + margin: 20px 0; + border: 1px solid #eee; + border-left-width: 5px; + border-radius: 3px; +} +.gs-callout h4 { + margin-top: 0; + margin-bottom: 5px; +} +.gs-callout p:last-child { + margin-bottom: 0; +} +.gs-callout code { + border-radius: 3px; +} +.gs-callout+.gs-callout { + margin-top: -5px; +} +.gs-callout-remember { + border-left-color: #f0ad4e; + align-items: center; + font-size: 1.2rem; +} +.gs-callout-remember h4 { + color: #f0ad4e; +} + +/* reference to user guide */ +.gs-torefguide { + align-items: center; + font-size: 0.9rem; +} + +.gs-torefguide .badge { + background-color: #130654; + margin: 10px 10px 10px 0px; + padding: 5px; +} + +.gs-torefguide a { + margin-left: 5px; + color: #130654; + border-bottom: 1px solid #FFCA00f3; + box-shadow: 0px -10px 0px #FFCA00f3 inset; +} + +.gs-torefguide p { + margin-top: 1rem; +} + +.gs-torefguide a:hover { + margin-left: 5px; + color: grey; + text-decoration: none; + border-bottom: 1px solid #b2ff80f3; + box-shadow: 0px -10px 0px #b2ff80f3 inset; +} + +/* question-task environment */ + +ul.task-bullet, ol.custom-bullet{ + list-style:none; + padding-left: 0; + margin-top: 2em; +} + +ul.task-bullet > li:before { + content:""; + height:2em; + width:2em; + display:block; + float:left; + margin-left:-2em; + background-position:center; + background-repeat:no-repeat; + background-color: #130654; + border-radius: 50%; + background-size:100%; + background-image:url('../question_mark_noback.svg'); + } + +ul.task-bullet > li { + border-left: 1px solid #130654; + padding-left:1em; +} + +ul.task-bullet > li > p:first-child { + font-size: 1.1rem; + padding-left: 0.75rem; +} + +/* Getting started index page */ + +.intro-card { + background:#FFF; + border-radius:0; + padding: 30px 10px 10px 10px; + margin: 10px 0px; +} + +.intro-card .card-text { + margin:20px 0px; + /*min-height: 150px; */ +} + +.intro-card .card-img-top { + margin: 10px; +} + +.install-block { + padding-bottom: 30px; +} + +.install-card .card-header { + border: none; + background-color:white; + color: #150458; + font-size: 1.1rem; + font-weight: bold; + padding: 1rem 1rem 0rem 1rem; +} + +.install-card .card-footer { + border: none; + background-color:white; +} + +.install-card pre { + margin: 0 1em 1em 1em; +} + +.custom-button { + background-color:#DCDCDC; + border: none; + color: #484848; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 0.9rem; + border-radius: 0.5rem; + max-width: 120px; + padding: 0.5rem 0rem; +} + +.custom-button a { + color: #484848; +} + +.custom-button p { + margin-top: 0; + margin-bottom: 0rem; + color: #484848; +} + +/* intro to tutorial collapsed cards */ + +.tutorial-accordion { + margin-top: 20px; + margin-bottom: 20px; +} + +.tutorial-card .card-header.card-link .btn { + margin-right: 12px; +} + +.tutorial-card .card-header.card-link .btn:after { + content: "-"; +} + +.tutorial-card .card-header.card-link.collapsed .btn:after { + content: "+"; +} + +.tutorial-card-header-1 { + justify-content: space-between; + align-items: center; +} + +.tutorial-card-header-2 { + justify-content: flex-start; + align-items: center; + font-size: 1.3rem; +} + +.tutorial-card .card-header { + cursor: pointer; + background-color: white; +} + +.tutorial-card .card-body { + background-color: #F0F0F0; +} + +.tutorial-card .badge { + background-color: #130654; + margin: 10px 10px 10px 10px; + padding: 5px; +} + +.tutorial-card .gs-badge-link p { + margin: 0px; +} + +.tutorial-card .gs-badge-link a { + color: white; + text-decoration: none; +} + +.tutorial-card .badge:hover { + background-color: grey; +} diff --git a/doc/source/_static/logo_r.svg b/doc/source/_static/logo_r.svg new file mode 100644 index 0000000000000..389b03c113e0f --- /dev/null +++ b/doc/source/_static/logo_r.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/doc/source/_static/logo_sas.svg b/doc/source/_static/logo_sas.svg new file mode 100644 index 0000000000000..d14fa105d49d6 --- /dev/null +++ b/doc/source/_static/logo_sas.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/doc/source/_static/logo_sql.svg b/doc/source/_static/logo_sql.svg new file mode 100644 index 0000000000000..4a5b7d0b1b943 --- /dev/null +++ b/doc/source/_static/logo_sql.svg @@ -0,0 +1,73 @@ + + + + + + + + image/svg+xml + + + + + + + + + SQL + diff --git a/doc/source/_static/logo_stata.svg b/doc/source/_static/logo_stata.svg new file mode 100644 index 0000000000000..a6e3f1d221959 --- /dev/null +++ b/doc/source/_static/logo_stata.svg @@ -0,0 +1,17 @@ + + + + + stata-logo-blue + + + + + + + + \ No newline at end of file diff --git a/doc/source/_static/schemas/01_table_dataframe.svg b/doc/source/_static/schemas/01_table_dataframe.svg new file mode 100644 index 0000000000000..9bd1c217b3ca2 --- /dev/null +++ b/doc/source/_static/schemas/01_table_dataframe.svg @@ -0,0 +1,262 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + column + DataFrame + + + row + + + diff --git a/doc/source/_static/schemas/01_table_series.svg b/doc/source/_static/schemas/01_table_series.svg new file mode 100644 index 0000000000000..d52c882f26868 --- /dev/null +++ b/doc/source/_static/schemas/01_table_series.svg @@ -0,0 +1,127 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + Series + + + diff --git a/doc/source/_static/schemas/01_table_spreadsheet.png b/doc/source/_static/schemas/01_table_spreadsheet.png new file mode 100644 index 0000000000000..b3cf5a0245b9c Binary files /dev/null and b/doc/source/_static/schemas/01_table_spreadsheet.png differ diff --git a/doc/source/_static/schemas/02_io_readwrite.svg b/doc/source/_static/schemas/02_io_readwrite.svg new file mode 100644 index 0000000000000..a99a6d731a6ad --- /dev/null +++ b/doc/source/_static/schemas/02_io_readwrite.svg @@ -0,0 +1,1401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + read_* + to_* + + + + + + + + + + + + + + + + + + + + + + + CSV + + + + + + + + + + + + + XLS + + + + + + + + + + + + + + + + + + PARQUET + + + + + + + + HTML + + <> + + + + + HDF5 + + + + + + + + JSON + + {} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GBQ + + + + + + + SQL + + + + + + ... + + + + + + + + + + CSV + + + + + + + + + + + + + XLS + + + + + + + + + + + + + + + + + + PARQUET + + + + + + + + HTML + + <> + + + + + HDF5 + + + + + + + + JSON + + {} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GBQ + + + + + + + SQL + + + + + + ... + + + + + + + diff --git a/doc/source/_static/schemas/03_subset_columns.svg b/doc/source/_static/schemas/03_subset_columns.svg new file mode 100644 index 0000000000000..5495d3f67bcfc --- /dev/null +++ b/doc/source/_static/schemas/03_subset_columns.svg @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/03_subset_columns_rows.svg b/doc/source/_static/schemas/03_subset_columns_rows.svg new file mode 100644 index 0000000000000..5ea9d609ec1c3 --- /dev/null +++ b/doc/source/_static/schemas/03_subset_columns_rows.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/03_subset_rows.svg b/doc/source/_static/schemas/03_subset_rows.svg new file mode 100644 index 0000000000000..41fe07d7fc34e --- /dev/null +++ b/doc/source/_static/schemas/03_subset_rows.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/04_plot_overview.svg b/doc/source/_static/schemas/04_plot_overview.svg new file mode 100644 index 0000000000000..44ae5b6ae5e33 --- /dev/null +++ b/doc/source/_static/schemas/04_plot_overview.svg @@ -0,0 +1,6443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + .plot.* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ... + + + diff --git a/doc/source/_static/schemas/05_newcolumn_1.svg b/doc/source/_static/schemas/05_newcolumn_1.svg new file mode 100644 index 0000000000000..c158aa932d38e --- /dev/null +++ b/doc/source/_static/schemas/05_newcolumn_1.svg @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/05_newcolumn_2.svg b/doc/source/_static/schemas/05_newcolumn_2.svg new file mode 100644 index 0000000000000..8bd5ad9a26994 --- /dev/null +++ b/doc/source/_static/schemas/05_newcolumn_2.svg @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/05_newcolumn_3.svg b/doc/source/_static/schemas/05_newcolumn_3.svg new file mode 100644 index 0000000000000..45272d8c9a368 --- /dev/null +++ b/doc/source/_static/schemas/05_newcolumn_3.svg @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/06_aggregate.svg b/doc/source/_static/schemas/06_aggregate.svg new file mode 100644 index 0000000000000..14428feda44ec --- /dev/null +++ b/doc/source/_static/schemas/06_aggregate.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/06_groupby.svg b/doc/source/_static/schemas/06_groupby.svg new file mode 100644 index 0000000000000..ca4d32be7084b --- /dev/null +++ b/doc/source/_static/schemas/06_groupby.svg @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/06_groupby_agg_detail.svg b/doc/source/_static/schemas/06_groupby_agg_detail.svg new file mode 100644 index 0000000000000..23a78d3ed2a9e --- /dev/null +++ b/doc/source/_static/schemas/06_groupby_agg_detail.svg @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/06_groupby_select_detail.svg b/doc/source/_static/schemas/06_groupby_select_detail.svg new file mode 100644 index 0000000000000..589c3add26e6f --- /dev/null +++ b/doc/source/_static/schemas/06_groupby_select_detail.svg @@ -0,0 +1,697 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/06_reduction.svg b/doc/source/_static/schemas/06_reduction.svg new file mode 100644 index 0000000000000..6ee808b953f7e --- /dev/null +++ b/doc/source/_static/schemas/06_reduction.svg @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/06_valuecounts.svg b/doc/source/_static/schemas/06_valuecounts.svg new file mode 100644 index 0000000000000..6d7439b45ae6f --- /dev/null +++ b/doc/source/_static/schemas/06_valuecounts.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + 3 + + 2 + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/07_melt.svg b/doc/source/_static/schemas/07_melt.svg new file mode 100644 index 0000000000000..c4551b48c5001 --- /dev/null +++ b/doc/source/_static/schemas/07_melt.svg @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/07_pivot.svg b/doc/source/_static/schemas/07_pivot.svg new file mode 100644 index 0000000000000..14b61c5f9a73b --- /dev/null +++ b/doc/source/_static/schemas/07_pivot.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/07_pivot_table.svg b/doc/source/_static/schemas/07_pivot_table.svg new file mode 100644 index 0000000000000..81ddb8b7f9288 --- /dev/null +++ b/doc/source/_static/schemas/07_pivot_table.svg @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/08_concat_column.svg b/doc/source/_static/schemas/08_concat_column.svg new file mode 100644 index 0000000000000..8c3e92a36d8ef --- /dev/null +++ b/doc/source/_static/schemas/08_concat_column.svg @@ -0,0 +1,465 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/08_concat_row.svg b/doc/source/_static/schemas/08_concat_row.svg new file mode 100644 index 0000000000000..116afc8f89890 --- /dev/null +++ b/doc/source/_static/schemas/08_concat_row.svg @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/_static/schemas/08_merge_left.svg b/doc/source/_static/schemas/08_merge_left.svg new file mode 100644 index 0000000000000..d06fcf2319a09 --- /dev/null +++ b/doc/source/_static/schemas/08_merge_left.svg @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + key + + + + + + + + + + + + + + + + + + + + + + key + + + + + + + + + + + + + + + + + + + + + + + + + + + key + + + + + key + + + + + + + + + + + + + + + + + + + key + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/conf.py b/doc/source/conf.py index c12c148d0f10d..a95cd4ab696f7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -232,6 +232,7 @@ html_static_path = ["_static"] html_css_files = [ + "css/getting_started.css", "css/pandas.css", ] diff --git a/doc/source/development/code_style.rst b/doc/source/development/code_style.rst index a295038b5a0bd..17f8783f71bfb 100644 --- a/doc/source/development/code_style.rst +++ b/doc/source/development/code_style.rst @@ -9,6 +9,12 @@ pandas code style guide .. contents:: Table of contents: :local: +*pandas* follows the `PEP8 `_ +standard and uses `Black `_ +and `Flake8 `_ to ensure a +consistent code format throughout the project. For details see the +:ref:`contributing guide to pandas`. + Patterns ======== @@ -119,14 +125,14 @@ For example: .. code-block:: python value = str - f"Unknown recived value, got: {repr(value)}" + f"Unknown received value, got: {repr(value)}" **Good:** .. code-block:: python value = str - f"Unknown recived type, got: '{type(value).__name__}'" + f"Unknown received type, got: '{type(value).__name__}'" Imports (aim for absolute) @@ -135,11 +141,11 @@ Imports (aim for absolute) In Python 3, absolute imports are recommended. In absolute import doing something like ``import string`` will import the string module rather than ``string.py`` in the same directory. As much as possible, you should try to write out -absolute imports that show the whole import chain from toplevel pandas. +absolute imports that show the whole import chain from top-level pandas. -Explicit relative imports are also supported in Python 3. But it is not -recommended to use it. Implicit relative imports should never be used -and is removed in Python 3. +Explicit relative imports are also supported in Python 3 but it is not +recommended to use them. Implicit relative imports should never be used +and are removed in Python 3. For example: diff --git a/doc/source/development/contributing_docstring.rst b/doc/source/development/contributing_docstring.rst index 649dd37b497b2..1c99b341f6c5a 100644 --- a/doc/source/development/contributing_docstring.rst +++ b/doc/source/development/contributing_docstring.rst @@ -937,33 +937,31 @@ classes. This helps us keep docstrings consistent, while keeping things clear for the user reading. It comes at the cost of some complexity when writing. Each shared docstring will have a base template with variables, like -``%(klass)s``. The variables filled in later on using the ``Substitution`` -decorator. Finally, docstrings can be appended to with the ``Appender`` -decorator. +``{klass}``. The variables filled in later on using the ``doc`` decorator. +Finally, docstrings can also be appended to with the ``doc`` decorator. In this example, we'll create a parent docstring normally (this is like ``pandas.core.generic.NDFrame``. Then we'll have two children (like ``pandas.core.series.Series`` and ``pandas.core.frame.DataFrame``). We'll -substitute the children's class names in this docstring. +substitute the class names in this docstring. .. code-block:: python class Parent: + @doc(klass="Parent") def my_function(self): - """Apply my function to %(klass)s.""" + """Apply my function to {klass}.""" ... class ChildA(Parent): - @Substitution(klass="ChildA") - @Appender(Parent.my_function.__doc__) + @doc(Parent.my_function, klass="ChildA") def my_function(self): ... class ChildB(Parent): - @Substitution(klass="ChildB") - @Appender(Parent.my_function.__doc__) + @doc(Parent.my_function, klass="ChildB") def my_function(self): ... @@ -972,18 +970,16 @@ The resulting docstrings are .. code-block:: python >>> print(Parent.my_function.__doc__) - Apply my function to %(klass)s. + Apply my function to Parent. >>> print(ChildA.my_function.__doc__) Apply my function to ChildA. >>> print(ChildB.my_function.__doc__) Apply my function to ChildB. -Notice two things: +Notice: 1. We "append" the parent docstring to the children docstrings, which are initially empty. -2. Python decorators are applied inside out. So the order is Append then - Substitution, even though Substitution comes first in the file. Our files will often contain a module-level ``_shared_doc_kwargs`` with some common substitution values (things like ``klass``, ``axes``, etc). @@ -992,14 +988,13 @@ You can substitute and append in one shot with something like .. code-block:: python - @Appender(template % _shared_doc_kwargs) + @doc(template, **_shared_doc_kwargs) def my_function(self): ... where ``template`` may come from a module-level ``_shared_docs`` dictionary mapping function names to docstrings. Wherever possible, we prefer using -``Appender`` and ``Substitution``, since the docstring-writing processes is -slightly closer to normal. +``doc``, since the docstring-writing processes is slightly closer to normal. See ``pandas.core.generic.NDFrame.fillna`` for an example template, and ``pandas.core.series.Series.fillna`` and ``pandas.core.generic.frame.fillna`` diff --git a/doc/source/ecosystem.rst b/doc/source/ecosystem.rst index fb06ee122ae88..b7e53b84f0e02 100644 --- a/doc/source/ecosystem.rst +++ b/doc/source/ecosystem.rst @@ -56,6 +56,11 @@ joining paths, replacing file extensions, and checking if files exist are also a Statistics and machine learning ------------------------------- +`pandas-tfrecords `__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Easy saving pandas dataframe to tensorflow tfrecords format and reading tfrecords to pandas. + `Statsmodels `__ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/getting_started/10min.rst b/doc/source/getting_started/10min.rst index 3055a22129b91..9994287c827e3 100644 --- a/doc/source/getting_started/10min.rst +++ b/doc/source/getting_started/10min.rst @@ -39,7 +39,7 @@ and labeled columns: df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD')) df -Creating a ``DataFrame`` by passing a dict of objects that can be converted to series-like. +Creating a :class:`DataFrame` by passing a dict of objects that can be converted to series-like. .. ipython:: python @@ -51,7 +51,7 @@ Creating a ``DataFrame`` by passing a dict of objects that can be converted to s 'F': 'foo'}) df2 -The columns of the resulting ``DataFrame`` have different +The columns of the resulting :class:`DataFrame` have different :ref:`dtypes `. .. ipython:: python @@ -70,17 +70,17 @@ will be completed: df2.abs df2.boxplot df2.add df2.C df2.add_prefix df2.clip - df2.add_suffix df2.clip_lower - df2.align df2.clip_upper - df2.all df2.columns + df2.add_suffix df2.columns + df2.align df2.copy + df2.all df2.count df2.any df2.combine - df2.append df2.combine_first - df2.apply df2.consolidate - df2.applymap - df2.D + df2.append df2.D + df2.apply df2.describe + df2.applymap df2.diff + df2.B df2.duplicated As you can see, the columns ``A``, ``B``, ``C``, and ``D`` are automatically -tab completed. ``E`` is there as well; the rest of the attributes have been +tab completed. ``E`` and ``F`` are there as well; the rest of the attributes have been truncated for brevity. Viewing data @@ -169,7 +169,7 @@ See the indexing documentation :ref:`Indexing and Selecting Data ` and Getting ~~~~~~~ -Selecting a single column, which yields a ``Series``, +Selecting a single column, which yields a :class:`Series`, equivalent to ``df.A``: .. ipython:: python @@ -469,10 +469,10 @@ Concatenating pandas objects together with :func:`concat`: pd.concat(pieces) .. note:: - Adding a column to a ``DataFrame`` is relatively fast. However, adding + Adding a column to a :class:`DataFrame` is relatively fast. However, adding a row requires a copy, and may be expensive. We recommend passing a - pre-built list of records to the ``DataFrame`` constructor instead - of building a ``DataFrame`` by iteratively appending records to it. + pre-built list of records to the :class:`DataFrame` constructor instead + of building a :class:`DataFrame` by iteratively appending records to it. See :ref:`Appending to dataframe ` for more. Join @@ -520,7 +520,7 @@ See the :ref:`Grouping section `. 'D': np.random.randn(8)}) df -Grouping and then applying the :meth:`~DataFrame.sum` function to the resulting +Grouping and then applying the :meth:`~pandas.core.groupby.GroupBy.sum` function to the resulting groups. .. ipython:: python @@ -528,7 +528,7 @@ groups. df.groupby('A').sum() Grouping by multiple columns forms a hierarchical index, and again we can -apply the ``sum`` function. +apply the :meth:`~pandas.core.groupby.GroupBy.sum` function. .. ipython:: python @@ -648,7 +648,7 @@ the quarter end: Categoricals ------------ -pandas can include categorical data in a ``DataFrame``. For full docs, see the +pandas can include categorical data in a :class:`DataFrame`. For full docs, see the :ref:`categorical introduction ` and the :ref:`API documentation `. .. ipython:: python @@ -664,14 +664,13 @@ Convert the raw grades to a categorical data type. df["grade"] Rename the categories to more meaningful names (assigning to -``Series.cat.categories`` is inplace!). +:meth:`Series.cat.categories` is inplace!). .. ipython:: python df["grade"].cat.categories = ["very good", "good", "very bad"] -Reorder the categories and simultaneously add the missing categories (methods under ``Series -.cat`` return a new ``Series`` by default). +Reorder the categories and simultaneously add the missing categories (methods under :meth:`Series.cat` return a new :class:`Series` by default). .. ipython:: python diff --git a/doc/source/getting_started/basics.rst b/doc/source/getting_started/basics.rst index 277080006cb3c..c6d9a48fcf8ed 100644 --- a/doc/source/getting_started/basics.rst +++ b/doc/source/getting_started/basics.rst @@ -689,6 +689,17 @@ of a 1D array of values. It can also be used as a function on regular arrays: s.value_counts() pd.value_counts(data) +.. versionadded:: 1.1.0 + +The :meth:`~DataFrame.value_counts` method can be used to count combinations across multiple columns. +By default all columns are used but a subset can be selected using the ``subset`` argument. + +.. ipython:: python + + data = {"a": [1, 2, 3, 4], "b": ["x", "x", "y", "y"]} + frame = pd.DataFrame(data) + frame.value_counts() + Similarly, you can get the most frequently occurring value(s) (the mode) of the values in a Series or DataFrame: .. ipython:: python diff --git a/doc/source/getting_started/dsintro.rst b/doc/source/getting_started/dsintro.rst index 5d7c9e405cfc2..200d567a62732 100644 --- a/doc/source/getting_started/dsintro.rst +++ b/doc/source/getting_started/dsintro.rst @@ -444,6 +444,7 @@ dtype. For example: data pd.DataFrame.from_records(data, index='C') +.. _basics.dataframe.sel_add_del: Column selection, addition, deletion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst index 34bb4f930f175..a2f8f79f22ae4 100644 --- a/doc/source/getting_started/index.rst +++ b/doc/source/getting_started/index.rst @@ -6,15 +6,666 @@ Getting started =============== +Installation +------------ + +Before you can use pandas, you’ll need to get it installed. + +.. raw:: html + +
+
+
+
+
+ Working with conda? +
+
+

+ +Pandas is part of the `Anaconda `__ distribution and can be +installed with Anaconda or Miniconda: + +.. raw:: html + +

+
+ +
+
+
+
+
+ Prefer pip? +
+
+

+ +Pandas can be installed via pip from `PyPI `__. + +.. raw:: html + +

+
+ +
+
+
+
+
+ In-depth instructions? +
+
+

Installing a specific version? + Installing from source? + Check the advanced installation page.

+ +.. container:: custom-button + + :ref:`Learn more ` + +.. raw:: html + +
+
+
+
+
+ +.. _gentle_intro: + +Intro to pandas +--------------- + +.. raw:: html + +
+
+ +
+ +
+
+ +When working with tabular data, such as data stored in spreadsheets or databases, Pandas is the right tool for you. Pandas will help you +to explore, clean and process your data. In Pandas, a data table is called a :class:`DataFrame`. + +.. image:: ../_static/schemas/01_table_dataframe.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_01_tableoriented>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Pandas supports the integration with many file formats or data sources out of the box (csv, excel, sql, json, parquet,…). Importing data from each of these +data sources is provided by function with the prefix ``read_*``. Similarly, the ``to_*`` methods are used to store data. + +.. image:: ../_static/schemas/02_io_readwrite.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_02_read_write>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Selecting or filtering specific rows and/or columns? Filtering the data on a condition? Methods for slicing, selecting, and extracting the +data you need are available in Pandas. + +.. image:: ../_static/schemas/03_subset_columns_rows.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_03_subset>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Pandas provides plotting your data out of the box, using the power of Matplotlib. You can pick the plot type (scatter, bar, boxplot,...) +corresponding to your data. + +.. image:: ../_static/schemas/04_plot_overview.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_04_plotting>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +There is no need to loop over all rows of your data table to do calculations. Data manipulations on a column work elementwise. +Adding a column to a :class:`DataFrame` based on existing data in other columns is straightforward. + +.. image:: ../_static/schemas/05_newcolumn_2.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_05_columns>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Basic statistics (mean, median, min, max, counts...) are easily calculable. These or custom aggregations can be applied on the entire +data set, a sliding window of the data or grouped by categories. The latter is also known as the split-apply-combine approach. + +.. image:: ../_static/schemas/06_groupby.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_06_stats>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Change the structure of your data table in multiple ways. You can :func:`~pandas.melt` your data table from wide to long/tidy form or :func:`~pandas.pivot` +from long to wide format. With aggregations built-in, a pivot table is created with a sinlge command. + +.. image:: ../_static/schemas/07_melt.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_07_reshape>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Multiple tables can be concatenated both column wise as row wise and database-like join/merge operations are provided to combine multiple tables of data. + +.. image:: ../_static/schemas/08_concat_row.svg + :align: center + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_08_combine>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Pandas has great support for time series and has an extensive set of tools for working with dates, times, and time-indexed data. + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_09_timeseries>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+ +
+
+ +Data sets do not only contain numerical data. Pandas provides a wide range of functions to cleaning textual data and extract useful information from it. + +.. raw:: html + +
+ + +:ref:`To introduction tutorial <10min_tut_10_text>` + +.. raw:: html + + + + +:ref:`To user guide ` + +.. raw:: html + + +
+
+
+
+ +
+
+ + +.. _comingfrom: + +Coming from... +-------------- + +Currently working with other software for data manipulation in a tabular format? You're probably familiar to typical +data operations and know *what* to do with your tabular data, but lacking the syntax to execute these operations. Get to know +the pandas syntax by looking for equivalents from the software you already know: + +.. raw:: html + +
+
+
+
+ R project logo +
+

The R programming language provides the data.frame data structure and multiple packages, + such as tidyverse use and extend data.frames for convenient data handling + functionalities similar to pandas.

+ +.. container:: custom-button + + :ref:`Learn more ` + +.. raw:: html + +
+
+
+
+
+ SQL logo +
+

Already familiar to SELECT, GROUP BY, JOIN,...? + Most of these SQL manipulations do have equivalents in pandas.

+ +.. container:: custom-button + + :ref:`Learn more ` + +.. raw:: html + +
+
+
+
+
+ STATA logo +
+

The data set included in the + STATA statistical software suite corresponds + to the pandas data.frame. Many of the operations known from STATA have an equivalent + in pandas.

+ +.. container:: custom-button + + :ref:`Learn more ` + +.. raw:: html + +
+
+
+
+
+ SAS logo +
+

The SAS statistical software suite + also provides the data set corresponding to the pandas data.frame. + Also vectorized operations, filtering, string processing operations,... from SAS have similar + functions in pandas.

+ +.. container:: custom-button + + :ref:`Learn more ` + +.. raw:: html + +
+
+
+
+
+ +Community tutorials +------------------- + +The community produces a wide variety of tutorials available online. Some of the +material is enlisted in the community contributed :ref:`tutorials`. + + .. If you update this toctree, also update the manual toctree in the main index.rst.template .. toctree:: :maxdepth: 2 + :hidden: install overview 10min + intro_tutorials/index basics dsintro comparison/index diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index ca285243b5f50..bc1be527696a5 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -163,6 +163,23 @@ The commands in this table will install pandas for Python 3 from your distributi to get the newest version of pandas, it's recommended to install using the ``pip`` or ``conda`` methods described above. +Handling ImportErrors +~~~~~~~~~~~~~~~~~~~~~~ + +If you encounter an ImportError, it usually means that Python couldn't find pandas in the list of available +libraries. Python internally has a list of directories it searches through, to find packages. You can +obtain these directories with:: + + import sys + sys.path + +One way you could be encountering this error is if you have multiple Python installations on your system +and you don't have pandas installed in the Python installation you're currently using. +In Linux/Mac you can run ``which python`` on your terminal and it will tell you which Python installation you're +using. If it's something like "/usr/bin/python", you're using the Python from the system, which is not recommended. + +It is highly recommended to use ``conda``, for quick installation and for package and dependency updates. +You can find simple installation instructions for pandas in this document: `installation instructions `. Installing from source ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/getting_started/intro_tutorials/01_table_oriented.rst b/doc/source/getting_started/intro_tutorials/01_table_oriented.rst new file mode 100644 index 0000000000000..02e59b3c81755 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/01_table_oriented.rst @@ -0,0 +1,218 @@ +.. _10min_tut_01_tableoriented: + +{{ header }} + +What kind of data does pandas handle? +===================================== + +.. raw:: html + +
    +
  • + +I want to start using pandas + +.. ipython:: python + + import pandas as pd + +To load the pandas package and start working with it, import the +package. The community agreed alias for pandas is ``pd``, so loading +pandas as ``pd`` is assumed standard practice for all of the pandas +documentation. + +.. raw:: html + +
  • +
+ +Pandas data table representation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/01_table_dataframe.svg + :align: center + +.. raw:: html + +
    +
  • + +I want to store passenger data of the Titanic. For a number of passengers, I know the name (characters), age (integers) and sex (male/female) data. + +.. ipython:: python + + df = pd.DataFrame({ + "Name": ["Braund, Mr. Owen Harris", + "Allen, Mr. William Henry", + "Bonnell, Miss. Elizabeth"], + "Age": [22, 35, 58], + "Sex": ["male", "male", "female"]} + ) + df + +To manually store data in a table, create a ``DataFrame``. When using a Python dictionary of lists, the dictionary keys will be used as column headers and +the values in each list as rows of the ``DataFrame``. + +.. raw:: html + +
  • +
+ +A :class:`DataFrame` is a 2-dimensional data structure that can store data of +different types (including characters, integers, floating point values, +categorical data and more) in columns. It is similar to a spreadsheet, a +SQL table or the ``data.frame`` in R. + +- The table has 3 columns, each of them with a column label. The column + labels are respectively ``Name``, ``Age`` and ``Sex``. +- The column ``Name`` consists of textual data with each value a + string, the column ``Age`` are numbers and the column ``Sex`` is + textual data. + +In spreadsheet software, the table representation of our data would look +very similar: + +.. image:: ../../_static/schemas/01_table_spreadsheet.png + :align: center + +Each column in a ``DataFrame`` is a ``Series`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/01_table_series.svg + :align: center + +.. raw:: html + +
    +
  • + +I’m just interested in working with the data in the column ``Age`` + +.. ipython:: python + + df["Age"] + +When selecting a single column of a pandas :class:`DataFrame`, the result is +a pandas :class:`Series`. To select the column, use the column label in +between square brackets ``[]``. + +.. raw:: html + +
  • +
+ +.. note:: + If you are familiar to Python + :ref:`dictionaries `, the selection of a + single column is very similar to selection of dictionary values based on + the key. + +You can create a ``Series`` from scratch as well: + +.. ipython:: python + + ages = pd.Series([22, 35, 58], name="Age") + ages + +A pandas ``Series`` has no column labels, as it is just a single column +of a ``DataFrame``. A Series does have row labels. + +Do something with a DataFrame or Series +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. raw:: html + +
    +
  • + +I want to know the maximum Age of the passengers + +We can do this on the ``DataFrame`` by selecting the ``Age`` column and +applying ``max()``: + +.. ipython:: python + + df["Age"].max() + +Or to the ``Series``: + +.. ipython:: python + + ages.max() + +.. raw:: html + +
  • +
+ +As illustrated by the ``max()`` method, you can *do* things with a +``DataFrame`` or ``Series``. pandas provides a lot of functionalities, +each of them a *method* you can apply to a ``DataFrame`` or ``Series``. +As methods are functions, do not forget to use parentheses ``()``. + +.. raw:: html + +
    +
  • + +I’m interested in some basic statistics of the numerical data of my data table + +.. ipython:: python + + df.describe() + +The :func:`~DataFrame.describe` method provides a quick overview of the numerical data in +a ``DataFrame``. As the ``Name`` and ``Sex`` columns are textual data, +these are by default not taken into account by the :func:`~DataFrame.describe` method. + +.. raw:: html + +
  • +
+ +Many pandas operations return a ``DataFrame`` or a ``Series``. The +:func:`~DataFrame.describe` method is an example of a pandas operation returning a +pandas ``Series``. + +.. raw:: html + +
+ To user guide + +Check more options on ``describe`` in the user guide section about :ref:`aggregations with describe ` + +.. raw:: html + +
+ +.. note:: + This is just a starting point. Similar to spreadsheet + software, pandas represents data as a table with columns and rows. Apart + from the representation, also the data manipulations and calculations + you would do in spreadsheet software are supported by pandas. Continue + reading the next tutorials to get started! + +.. raw:: html + +
+

REMEMBER

+ +- Import the package, aka ``import pandas as pd`` +- A table of data is stored as a pandas ``DataFrame`` +- Each column in a ``DataFrame`` is a ``Series`` +- You can do things by applying a method to a ``DataFrame`` or ``Series`` + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A more extended explanation to ``DataFrame`` and ``Series`` is provided in the :ref:`introduction to data structures `. + +.. raw:: html + +
\ No newline at end of file diff --git a/doc/source/getting_started/intro_tutorials/02_read_write.rst b/doc/source/getting_started/intro_tutorials/02_read_write.rst new file mode 100644 index 0000000000000..797bdbcf25d17 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/02_read_write.rst @@ -0,0 +1,232 @@ +.. _10min_tut_02_read_write: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +This tutorial uses the titanic data set, stored as CSV. The data +consists of the following data columns: + +- PassengerId: Id of every passenger. +- Survived: This feature have value 0 and 1. 0 for not survived and 1 + for survived. +- Pclass: There are 3 classes: Class 1, Class 2 and Class 3. +- Name: Name of passenger. +- Sex: Gender of passenger. +- Age: Age of passenger. +- SibSp: Indication that passenger have siblings and spouse. +- Parch: Whether a passenger is alone or have family. +- Ticket: Ticket number of passenger. +- Fare: Indicating the fare. +- Cabin: The cabin of passenger. +- Embarked: The embarked category. + +.. raw:: html + +

    + To raw data +
    +
    +
  • +
+
+ +How do I read and write tabular data? +===================================== + +.. image:: ../../_static/schemas/02_io_readwrite.svg + :align: center + +.. raw:: html + +
    +
  • + +I want to analyse the titanic passenger data, available as a CSV file. + +.. ipython:: python + + titanic = pd.read_csv("data/titanic.csv") + +pandas provides the :func:`read_csv` function to read data stored as a csv +file into a pandas ``DataFrame``. pandas supports many different file +formats or data sources out of the box (csv, excel, sql, json, parquet, +…), each of them with the prefix ``read_*``. + +.. raw:: html + +
  • +
+ +Make sure to always have a check on the data after reading in the +data. When displaying a ``DataFrame``, the first and last 5 rows will be +shown by default: + +.. ipython:: python + + titanic + +.. raw:: html + +
    +
  • + +I want to see the first 8 rows of a pandas DataFrame. + +.. ipython:: python + + titanic.head(8) + +To see the first N rows of a ``DataFrame``, use the :meth:`~DataFrame.head` method with +the required number of rows (in this case 8) as argument. + +.. raw:: html + +
  • +
+ +.. note:: + + Interested in the last N rows instead? pandas also provides a + :meth:`~DataFrame.tail` method. For example, ``titanic.tail(10)`` will return the last + 10 rows of the DataFrame. + +A check on how pandas interpreted each of the column data types can be +done by requesting the pandas ``dtypes`` attribute: + +.. ipython:: python + + titanic.dtypes + +For each of the columns, the used data type is enlisted. The data types +in this ``DataFrame`` are integers (``int64``), floats (``float63``) and +strings (``object``). + +.. note:: + When asking for the ``dtypes``, no brackets are used! + ``dtypes`` is an attribute of a ``DataFrame`` and ``Series``. Attributes + of ``DataFrame`` or ``Series`` do not need brackets. Attributes + represent a characteristic of a ``DataFrame``/``Series``, whereas a + method (which requires brackets) *do* something with the + ``DataFrame``/``Series`` as introduced in the :ref:`first tutorial <10min_tut_01_tableoriented>`. + +.. raw:: html + +
    +
  • + +My colleague requested the titanic data as a spreadsheet. + +.. ipython:: python + + titanic.to_excel('titanic.xlsx', sheet_name='passengers', index=False) + +Whereas ``read_*`` functions are used to read data to pandas, the +``to_*`` methods are used to store data. The :meth:`~DataFrame.to_excel` method stores +the data as an excel file. In the example here, the ``sheet_name`` is +named *passengers* instead of the default *Sheet1*. By setting +``index=False`` the row index labels are not saved in the spreadsheet. + +.. raw:: html + +
  • +
+ +The equivalent read function :meth:`~DataFrame.to_excel` will reload the data to a +``DataFrame``: + +.. ipython:: python + + titanic = pd.read_excel('titanic.xlsx', sheet_name='passengers') + +.. ipython:: python + + titanic.head() + +.. ipython:: python + :suppress: + + import os + os.remove('titanic.xlsx') + +.. raw:: html + +
    +
  • + +I’m interested in a technical summary of a ``DataFrame`` + +.. ipython:: python + + titanic.info() + + +The method :meth:`~DataFrame.info` provides technical information about a +``DataFrame``, so let’s explain the output in more detail: + +- It is indeed a :class:`DataFrame`. +- There are 891 entries, i.e. 891 rows. +- Each row has a row label (aka the ``index``) with values ranging from + 0 to 890. +- The table has 12 columns. Most columns have a value for each of the + rows (all 891 values are ``non-null``). Some columns do have missing + values and less than 891 ``non-null`` values. +- The columns ``Name``, ``Sex``, ``Cabin`` and ``Embarked`` consists of + textual data (strings, aka ``object``). The other columns are + numerical data with some of them whole numbers (aka ``integer``) and + others are real numbers (aka ``float``). +- The kind of data (characters, integers,…) in the different columns + are summarized by listing the ``dtypes``. +- The approximate amount of RAM used to hold the DataFrame is provided + as well. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+

REMEMBER

+ +- Getting data in to pandas from many different file formats or data + sources is supported by ``read_*`` functions. +- Exporting data out of pandas is provided by different + ``to_*``\ methods. +- The ``head``/``tail``/``info`` methods and the ``dtypes`` attribute + are convenient for a first check. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +For a complete overview of the input and output possibilites from and to pandas, see the user guide section about :ref:`reader and writer functions `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/03_subset_data.rst b/doc/source/getting_started/intro_tutorials/03_subset_data.rst new file mode 100644 index 0000000000000..7a4347905ad8d --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/03_subset_data.rst @@ -0,0 +1,405 @@ +.. _10min_tut_03_subset: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +This tutorial uses the titanic data set, stored as CSV. The data +consists of the following data columns: + +- PassengerId: Id of every passenger. +- Survived: This feature have value 0 and 1. 0 for not survived and 1 + for survived. +- Pclass: There are 3 classes: Class 1, Class 2 and Class 3. +- Name: Name of passenger. +- Sex: Gender of passenger. +- Age: Age of passenger. +- SibSp: Indication that passenger have siblings and spouse. +- Parch: Whether a passenger is alone or have family. +- Ticket: Ticket number of passenger. +- Fare: Indicating the fare. +- Cabin: The cabin of passenger. +- Embarked: The embarked category. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + titanic = pd.read_csv("data/titanic.csv") + titanic.head() + +.. raw:: html + +
  • +
+
+ +How do I select a subset of a ``DataFrame``? +============================================ + +How do I select specific columns from a ``DataFrame``? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/03_subset_columns.svg + :align: center + +.. raw:: html + +
    +
  • + +I’m interested in the age of the titanic passengers. + +.. ipython:: python + + ages = titanic["Age"] + ages.head() + +To select a single column, use square brackets ``[]`` with the column +name of the column of interest. + +.. raw:: html + +
  • +
+ +Each column in a :class:`DataFrame` is a :class:`Series`. As a single column is +selected, the returned object is a pandas :class:`DataFrame`. We can verify this +by checking the type of the output: + +.. ipython:: python + + type(titanic["Age"]) + +And have a look at the ``shape`` of the output: + +.. ipython:: python + + titanic["Age"].shape + +:attr:`DataFrame.shape` is an attribute (remember :ref:`tutorial on reading and writing <10min_tut_02_read_write>`, do not use parantheses for attributes) of a +pandas ``Series`` and ``DataFrame`` containing the number of rows and +columns: *(nrows, ncolumns)*. A pandas Series is 1-dimensional and only +the number of rows is returned. + +.. raw:: html + +
    +
  • + +I’m interested in the age and sex of the titanic passengers. + +.. ipython:: python + + age_sex = titanic[["Age", "Sex"]] + age_sex.head() + +To select multiple columns, use a list of column names within the +selection brackets ``[]``. + +.. raw:: html + +
  • +
+ +.. note:: + The inner square brackets define a + :ref:`Python list ` with column names, whereas + the outer brackets are used to select the data from a pandas + ``DataFrame`` as seen in the previous example. + +The returned data type is a pandas DataFrame: + +.. ipython:: python + + type(titanic[["Age", "Sex"]]) + +.. ipython:: python + + titanic[["Age", "Sex"]].shape + +The selection returned a ``DataFrame`` with 891 rows and 2 columns. Remember, a +``DataFrame`` is 2-dimensional with both a row and column dimension. + +.. raw:: html + +
+ To user guide + +For basic information on indexing, see the user guide section on :ref:`indexing and selecting data `. + +.. raw:: html + +
+ +How do I filter specific rows from a ``DataFrame``? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/03_subset_rows.svg + :align: center + +.. raw:: html + +
    +
  • + +I’m interested in the passengers older than 35 years. + +.. ipython:: python + + above_35 = titanic[titanic["Age"] > 35] + above_35.head() + +To select rows based on a conditional expression, use a condition inside +the selection brackets ``[]``. + +.. raw:: html + +
  • +
+ +The condition inside the selection +brackets ``titanic["Age"] > 35`` checks for which rows the ``Age`` +column has a value larger than 35: + +.. ipython:: python + + titanic["Age"] > 35 + +The output of the conditional expression (``>``, but also ``==``, +``!=``, ``<``, ``<=``,… would work) is actually a pandas ``Series`` of +boolean values (either ``True`` or ``False``) with the same number of +rows as the original ``DataFrame``. Such a ``Series`` of boolean values +can be used to filter the ``DataFrame`` by putting it in between the +selection brackets ``[]``. Only rows for which the value is ``True`` +will be selected. + +We now from before that the original titanic ``DataFrame`` consists of +891 rows. Let’s have a look at the amount of rows which satisfy the +condition by checking the ``shape`` attribute of the resulting +``DataFrame`` ``above_35``: + +.. ipython:: python + + above_35.shape + +.. raw:: html + +
    +
  • + +I’m interested in the titanic passengers from cabin class 2 and 3. + +.. ipython:: python + + class_23 = titanic[titanic["Pclass"].isin([2, 3])] + class_23.head() + +Similar to the conditional expression, the :func:`~Series.isin` conditional function +returns a ``True`` for each row the values are in the provided list. To +filter the rows based on such a function, use the conditional function +inside the selection brackets ``[]``. In this case, the condition inside +the selection brackets ``titanic["Pclass"].isin([2, 3])`` checks for +which rows the ``Pclass`` column is either 2 or 3. + +.. raw:: html + +
  • +
+ +The above is equivalent to filtering by rows for which the class is +either 2 or 3 and combining the two statements with an ``|`` (or) +operator: + +.. ipython:: python + + class_23 = titanic[(titanic["Pclass"] == 2) | (titanic["Pclass"] == 3)] + class_23.head() + +.. note:: + When combining multiple conditional statements, each condition + must be surrounded by parentheses ``()``. Moreover, you can not use + ``or``/``and`` but need to use the ``or`` operator ``|`` and the ``and`` + operator ``&``. + +.. raw:: html + +
+ To user guide + +See the dedicated section in the user guide about :ref:`boolean indexing ` or about the :ref:`isin function `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +I want to work with passenger data for which the age is known. + +.. ipython:: python + + age_no_na = titanic[titanic["Age"].notna()] + age_no_na.head() + +The :meth:`~Series.notna` conditional function returns a ``True`` for each row the +values are not an ``Null`` value. As such, this can be combined with the +selection brackets ``[]`` to filter the data table. + +.. raw:: html + +
  • +
+ +You might wonder what actually changed, as the first 5 lines are still +the same values. One way to verify is to check if the shape has changed: + +.. ipython:: python + + age_no_na.shape + +.. raw:: html + +
+ To user guide + +For more dedicated functions on missing values, see the user guide section about :ref:`handling missing data `. + +.. raw:: html + +
+ +How do I select specific rows and columns from a ``DataFrame``? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/03_subset_columns_rows.svg + :align: center + +.. raw:: html + +
    +
  • + +I’m interested in the names of the passengers older than 35 years. + +.. ipython:: python + + adult_names = titanic.loc[titanic["Age"] > 35, "Name"] + adult_names.head() + +In this case, a subset of both rows and columns is made in one go and +just using selection brackets ``[]`` is not sufficient anymore. The +``loc``/``iloc`` operators are required in front of the selection +brackets ``[]``. When using ``loc``/``iloc``, the part before the comma +is the rows you want, and the part after the comma is the columns you +want to select. + +.. raw:: html + +
  • +
+ +When using the column names, row labels or a condition expression, use +the ``loc`` operator in front of the selection brackets ``[]``. For both +the part before and after the comma, you can use a single label, a list +of labels, a slice of labels, a conditional expression or a colon. Using +a colon specificies you want to select all rows or columns. + +.. raw:: html + +
    +
  • + +I’m interested in rows 10 till 25 and columns 3 to 5. + +.. ipython:: python + + titanic.iloc[9:25, 2:5] + +Again, a subset of both rows and columns is made in one go and just +using selection brackets ``[]`` is not sufficient anymore. When +specifically interested in certain rows and/or columns based on their +position in the table, use the ``iloc`` operator in front of the +selection brackets ``[]``. + +.. raw:: html + +
  • +
+ +When selecting specific rows and/or columns with ``loc`` or ``iloc``, +new values can be assigned to the selected data. For example, to assign +the name ``anonymous`` to the first 3 elements of the third column: + +.. ipython:: python + + titanic.iloc[0:3, 3] = "anonymous" + titanic.head() + +.. raw:: html + +
+ To user guide + +See the user guide section on :ref:`different choices for indexing ` to get more insight in the usage of ``loc`` and ``iloc``. + +.. raw:: html + +
+ +.. raw:: html + +
+

REMEMBER

+ +- When selecting subsets of data, square brackets ``[]`` are used. +- Inside these brackets, you can use a single column/row label, a list + of column/row labels, a slice of labels, a conditional expression or + a colon. +- Select specific rows and/or columns using ``loc`` when using the row + and column names +- Select specific rows and/or columns using ``iloc`` when using the + positions in the table +- You can assign new values to a selection based on ``loc``/``iloc``. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A full overview about indexing is provided in the user guide pages on :ref:`indexing and selecting data `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/04_plotting.rst b/doc/source/getting_started/intro_tutorials/04_plotting.rst new file mode 100644 index 0000000000000..f3d99ee56359a --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/04_plotting.rst @@ -0,0 +1,252 @@ +.. _10min_tut_04_plotting: + +{{ header }} + +.. ipython:: python + + import pandas as pd + import matplotlib.pyplot as plt + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +For this tutorial, air quality data about :math:`NO_2` is used, made +available by `openaq `__ and using the +`py-openaq `__ package. +The ``air_quality_no2.csv`` data set provides :math:`NO_2` values for +the measurement stations *FR04014*, *BETR801* and *London Westminster* +in respectively Paris, Antwerp and London. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + air_quality = pd.read_csv("data/air_quality_no2.csv", + index_col=0, parse_dates=True) + air_quality.head() + +.. note:: + The usage of the ``index_col`` and ``parse_dates`` parameters of the ``read_csv`` function to define the first (0th) column as + index of the resulting ``DataFrame`` and convert the dates in the column to :class:`Timestamp` objects, respectively. + +.. raw:: html + +
  • +
+
+ +How to create plots in pandas? +------------------------------ + +.. image:: ../../_static/schemas/04_plot_overview.svg + :align: center + +.. raw:: html + +
    +
  • + +I want a quick visual check of the data. + +.. ipython:: python + + @savefig 04_airqual_quick.png + air_quality.plot() + +With a ``DataFrame``, pandas creates by default one line plot for each of +the columns with numeric data. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
    +
  • + +I want to plot only the columns of the data table with the data from Paris. + +.. ipython:: python + + @savefig 04_airqual_paris.png + air_quality["station_paris"].plot() + +To plot a specific column, use the selection method of the +:ref:`subset data tutorial <10min_tut_03_subset>` in combination with the :meth:`~DataFrame.plot` +method. Hence, the :meth:`~DataFrame.plot` method works on both ``Series`` and +``DataFrame``. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
    +
  • + +I want to visually compare the :math:`N0_2` values measured in London versus Paris. + +.. ipython:: python + + @savefig 04_airqual_scatter.png + air_quality.plot.scatter(x="station_london", + y="station_paris", + alpha=0.5) + +.. raw:: html + +
  • +
+ +Apart from the default ``line`` plot when using the ``plot`` function, a +number of alternatives are available to plot data. Let’s use some +standard Python to get an overview of the available plot methods: + +.. ipython:: python + + [method_name for method_name in dir(air_quality.plot) + if not method_name.startswith("_")] + +.. note:: + In many development environments as well as ipython and + jupyter notebook, use the TAB button to get an overview of the available + methods, for example ``air_quality.plot.`` + TAB. + +One of the options is :meth:`DataFrame.plot.box`, which refers to a +`boxplot `__. The ``box`` +method is applicable on the air quality example data: + +.. ipython:: python + + @savefig 04_airqual_boxplot.png + air_quality.plot.box() + +.. raw:: html + +
+ To user guide + +For an introduction to plots other than the default line plot, see the user guide section about :ref:`supported plot styles `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +I want each of the columns in a separate subplot. + +.. ipython:: python + + @savefig 04_airqual_area_subplot.png + axs = air_quality.plot.area(figsize=(12, 4), subplots=True) + +Separate subplots for each of the data columns is supported by the ``subplots`` argument +of the ``plot`` functions. The builtin options available in each of the pandas plot +functions that are worthwhile to have a look. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +Some more formatting options are explained in the user guide section on :ref:`plot formatting `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +I want to further customize, extend or save the resulting plot. + +.. ipython:: python + + fig, axs = plt.subplots(figsize=(12, 4)); + air_quality.plot.area(ax=axs); + @savefig 04_airqual_customized.png + axs.set_ylabel("NO$_2$ concentration"); + fig.savefig("no2_concentrations.png") + +.. ipython:: python + :suppress: + + import os + os.remove('no2_concentrations.png') + +.. raw:: html + +
  • +
+ +Each of the plot objects created by pandas are a +`matplotlib `__ object. As Matplotlib provides +plenty of options to customize plots, making the link between pandas and +Matplotlib explicit enables all the power of matplotlib to the plot. +This strategy is applied in the previous example: + +:: + + fig, axs = plt.subplots(figsize=(12, 4)) # Create an empty matplotlib Figure and Axes + air_quality.plot.area(ax=axs) # Use pandas to put the area plot on the prepared Figure/Axes + axs.set_ylabel("NO$_2$ concentration") # Do any matplotlib customization you like + fig.savefig("no2_concentrations.png") # Save the Figure/Axes using the existing matplotlib method. + +.. raw:: html + +
+

REMEMBER

+ +- The ``.plot.*`` methods are applicable on both Series and DataFrames +- By default, each of the columns is plotted as a different element + (line, boxplot,…) +- Any plot created by pandas is a Matplotlib object. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A full overview of plotting in pandas is provided in the :ref:`visualization pages `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/05_add_columns.rst b/doc/source/getting_started/intro_tutorials/05_add_columns.rst new file mode 100644 index 0000000000000..d4f6a8d6bb4a2 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/05_add_columns.rst @@ -0,0 +1,186 @@ +.. _10min_tut_05_columns: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +For this tutorial, air quality data about :math:`NO_2` is used, made +available by `openaq `__ and using the +`py-openaq `__ package. +The ``air_quality_no2.csv`` data set provides :math:`NO_2` values for +the measurement stations *FR04014*, *BETR801* and *London Westminster* +in respectively Paris, Antwerp and London. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + air_quality = pd.read_csv("data/air_quality_no2.csv", + index_col=0, parse_dates=True) + air_quality.head() + +.. raw:: html + +
  • +
+
+ +How to create new columns derived from existing columns? +-------------------------------------------------------- + +.. image:: ../../_static/schemas/05_newcolumn_1.svg + :align: center + +.. raw:: html + +
    +
  • + +I want to express the :math:`NO_2` concentration of the station in London in mg/m\ :math:`^3` + +(*If we assume temperature of 25 degrees Celsius and pressure of 1013 +hPa, the conversion factor is 1.882*) + +.. ipython:: python + + air_quality["london_mg_per_cubic"] = air_quality["station_london"] * 1.882 + air_quality.head() + +To create a new column, use the ``[]`` brackets with the new column name +at the left side of the assignment. + +.. raw:: html + +
  • +
+ +.. note:: + The calculation of the values is done **element_wise**. This + means all values in the given column are multiplied by the value 1.882 + at once. You do not need to use a loop to iterate each of the rows! + +.. image:: ../../_static/schemas/05_newcolumn_2.svg + :align: center + +.. raw:: html + +
    +
  • + +I want to check the ratio of the values in Paris versus Antwerp and save the result in a new column + +.. ipython:: python + + air_quality["ratio_paris_antwerp"] = \ + air_quality["station_paris"] / air_quality["station_antwerp"] + air_quality.head() + +The calculation is again element-wise, so the ``/`` is applied *for the +values in each row*. + +.. raw:: html + +
  • +
+ +Also other mathematical operators (+, -, \*, /) or +logical operators (<, >, =,…) work element wise. The latter was already +used in the :ref:`subset data tutorial <10min_tut_03_subset>` to filter +rows of a table using a conditional expression. + +.. raw:: html + +
    +
  • + +I want to rename the data columns to the corresponding station identifiers used by openAQ + +.. ipython:: python + + air_quality_renamed = air_quality.rename( + columns={"station_antwerp": "BETR801", + "station_paris": "FR04014", + "station_london": "London Westminster"}) + +.. ipython:: python + + air_quality_renamed.head() + +The :meth:`~DataFrame.rename` function can be used for both row labels and column +labels. Provide a dictionary with the keys the current names and the +values the new names to update the corresponding names. + +.. raw:: html + +
  • +
+ +The mapping should not be restricted to fixed names only, but can be a +mapping function as well. For example, converting the column names to +lowercase letters can be done using a function as well: + +.. ipython:: python + + air_quality_renamed = air_quality_renamed.rename(columns=str.lower) + air_quality_renamed.head() + +.. raw:: html + +
+ To user guide + +Details about column or row label renaming is provided in the user guide section on :ref:`renaming labels `. + +.. raw:: html + +
+ +.. raw:: html + +
+

REMEMBER

+ +- Create a new column by assigning the output to the DataFrame with a + new column name in between the ``[]``. +- Operations are element-wise, no need to loop over rows. +- Use ``rename`` with a dictionary or function to rename row labels or + column names. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +The user guide contains a separate section on :ref:`column addition and deletion `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/06_calculate_statistics.rst b/doc/source/getting_started/intro_tutorials/06_calculate_statistics.rst new file mode 100644 index 0000000000000..7a94c90525027 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/06_calculate_statistics.rst @@ -0,0 +1,310 @@ +.. _10min_tut_06_stats: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +This tutorial uses the titanic data set, stored as CSV. The data +consists of the following data columns: + +- PassengerId: Id of every passenger. +- Survived: This feature have value 0 and 1. 0 for not survived and 1 + for survived. +- Pclass: There are 3 classes: Class 1, Class 2 and Class 3. +- Name: Name of passenger. +- Sex: Gender of passenger. +- Age: Age of passenger. +- SibSp: Indication that passenger have siblings and spouse. +- Parch: Whether a passenger is alone or have family. +- Ticket: Ticket number of passenger. +- Fare: Indicating the fare. +- Cabin: The cabin of passenger. +- Embarked: The embarked category. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + titanic = pd.read_csv("data/titanic.csv") + titanic.head() + +.. raw:: html + +
  • +
+
+ +How to calculate summary statistics? +------------------------------------ + +Aggregating statistics +~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/06_aggregate.svg + :align: center + +.. raw:: html + +
    +
  • + +What is the average age of the titanic passengers? + +.. ipython:: python + + titanic["Age"].mean() + +.. raw:: html + +
  • +
+ +Different statistics are available and can be applied to columns with +numerical data. Operations in general exclude missing data and operate +across rows by default. + +.. image:: ../../_static/schemas/06_reduction.svg + :align: center + +.. raw:: html + +
    +
  • + +What is the median age and ticket fare price of the titanic passengers? + +.. ipython:: python + + titanic[["Age", "Fare"]].median() + +The statistic applied to multiple columns of a ``DataFrame`` (the selection of two columns +return a ``DataFrame``, see the :ref:`subset data tutorial <10min_tut_03_subset>`) is calculated for each numeric column. + +.. raw:: html + +
  • +
+ +The aggregating statistic can be calculated for multiple columns at the +same time. Remember the ``describe`` function from :ref:`first tutorial <10min_tut_01_tableoriented>` tutorial? + +.. ipython:: python + + titanic[["Age", "Fare"]].describe() + +Instead of the predefined statistics, specific combinations of +aggregating statistics for given columns can be defined using the +:func:`DataFrame.agg` method: + +.. ipython:: python + + titanic.agg({'Age': ['min', 'max', 'median', 'skew'], + 'Fare': ['min', 'max', 'median', 'mean']}) + +.. raw:: html + +
+ To user guide + +Details about descriptive statistics are provided in the user guide section on :ref:`descriptive statistics `. + +.. raw:: html + +
+ + +Aggregating statistics grouped by category +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/06_groupby.svg + :align: center + +.. raw:: html + +
    +
  • + +What is the average age for male versus female titanic passengers? + +.. ipython:: python + + titanic[["Sex", "Age"]].groupby("Sex").mean() + +As our interest is the average age for each gender, a subselection on +these two columns is made first: ``titanic[["Sex", "Age"]]``. Next, the +:meth:`~DataFrame.groupby` method is applied on the ``Sex`` column to make a group per +category. The average age *for each gender* is calculated and +returned. + +.. raw:: html + +
  • +
+ +Calculating a given statistic (e.g. ``mean`` age) *for each category in +a column* (e.g. male/female in the ``Sex`` column) is a common pattern. +The ``groupby`` method is used to support this type of operations. More +general, this fits in the more general ``split-apply-combine`` pattern: + +- **Split** the data into groups +- **Apply** a function to each group independently +- **Combine** the results into a data structure + +The apply and combine steps are typically done together in pandas. + +In the previous example, we explicitly selected the 2 columns first. If +not, the ``mean`` method is applied to each column containing numerical +columns: + +.. ipython:: python + + titanic.groupby("Sex").mean() + +It does not make much sense to get the average value of the ``Pclass``. +if we are only interested in the average age for each gender, the +selection of columns (rectangular brackets ``[]`` as usual) is supported +on the grouped data as well: + +.. ipython:: python + + titanic.groupby("Sex")["Age"].mean() + +.. image:: ../../_static/schemas/06_groupby_select_detail.svg + :align: center + +.. note:: + The `Pclass` column contains numerical data but actually + represents 3 categories (or factors) with respectively the labels ‘1’, + ‘2’ and ‘3’. Calculating statistics on these does not make much sense. + Therefore, pandas provides a ``Categorical`` data type to handle this + type of data. More information is provided in the user guide + :ref:`categorical` section. + +.. raw:: html + +
    +
  • + +What is the mean ticket fare price for each of the sex and cabin class combinations? + +.. ipython:: python + + titanic.groupby(["Sex", "Pclass"])["Fare"].mean() + +Grouping can be done by multiple columns at the same time. Provide the +column names as a list to the :meth:`~DataFrame.groupby` method. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +A full description on the split-apply-combine approach is provided in the user guide section on :ref:`groupby operations `. + +.. raw:: html + +
+ +Count number of records by category +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/06_valuecounts.svg + :align: center + +.. raw:: html + +
    +
  • + +What is the number of passengers in each of the cabin classes? + +.. ipython:: python + + titanic["Pclass"].value_counts() + +The :meth:`~Series.value_counts` method counts the number of records for each +category in a column. + +.. raw:: html + +
  • +
+ +The function is a shortcut, as it is actually a groupby operation in combination with counting of the number of records +within each group: + +.. ipython:: python + + titanic.groupby("Pclass")["Pclass"].count() + +.. note:: + Both ``size`` and ``count`` can be used in combination with + ``groupby``. Whereas ``size`` includes ``NaN`` values and just provides + the number of rows (size of the table), ``count`` excludes the missing + values. In the ``value_counts`` method, use the ``dropna`` argument to + include or exclude the ``NaN`` values. + +.. raw:: html + +
+ To user guide + +The user guide has a dedicated section on ``value_counts`` , see page on :ref:`discretization `. + +.. raw:: html + +
+ +.. raw:: html + +
+

REMEMBER

+ +- Aggregation statistics can be calculated on entire columns or rows +- ``groupby`` provides the power of the *split-apply-combine* pattern +- ``value_counts`` is a convenient shortcut to count the number of + entries in each category of a variable + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A full description on the split-apply-combine approach is provided in the user guide pages about :ref:`groupby operations `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/07_reshape_table_layout.rst b/doc/source/getting_started/intro_tutorials/07_reshape_table_layout.rst new file mode 100644 index 0000000000000..b28a9012a4ad9 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/07_reshape_table_layout.rst @@ -0,0 +1,404 @@ +.. _10min_tut_07_reshape: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +This tutorial uses the titanic data set, stored as CSV. The data +consists of the following data columns: + +- PassengerId: Id of every passenger. +- Survived: This feature have value 0 and 1. 0 for not survived and 1 + for survived. +- Pclass: There are 3 classes: Class 1, Class 2 and Class 3. +- Name: Name of passenger. +- Sex: Gender of passenger. +- Age: Age of passenger. +- SibSp: Indication that passenger have siblings and spouse. +- Parch: Whether a passenger is alone or have family. +- Ticket: Ticket number of passenger. +- Fare: Indicating the fare. +- Cabin: The cabin of passenger. +- Embarked: The embarked category. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + titanic = pd.read_csv("data/titanic.csv") + titanic.head() + +.. raw:: html + +
  • +
  • + +
    +
    +

    + +This tutorial uses air quality data about :math:`NO_2` and Particulate matter less than 2.5 +micrometers, made available by +`openaq `__ and using the +`py-openaq `__ package. +The ``air_quality_long.csv`` data set provides :math:`NO_2` and +:math:`PM_{25}` values for the measurement stations *FR04014*, *BETR801* +and *London Westminster* in respectively Paris, Antwerp and London. + +The air-quality data set has the following columns: + +- city: city where the sensor is used, either Paris, Antwerp or London +- country: country where the sensor is used, either FR, BE or GB +- location: the id of the sensor, either *FR04014*, *BETR801* or + *London Westminster* +- parameter: the parameter measured by the sensor, either :math:`NO_2` + or Particulate matter +- value: the measured value +- unit: the unit of the measured parameter, in this case ‘µg/m³’ + +and the index of the ``DataFrame`` is ``datetime``, the datetime of the +measurement. + +.. note:: + The air-quality data is provided in a so-called *long format* + data representation with each observation on a separate row and each + variable a separate column of the data table. The long/narrow format is + also known as the `tidy data + format `__. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + air_quality = pd.read_csv("data/air_quality_long.csv", + index_col="date.utc", parse_dates=True) + air_quality.head() + +.. raw:: html + +
  • +
+
+ +How to reshape the layout of tables? +------------------------------------ + +Sort table rows +~~~~~~~~~~~~~~~ + +.. raw:: html + +
    +
  • + +I want to sort the titanic data according to the age of the passengers. + +.. ipython:: python + + titanic.sort_values(by="Age").head() + +.. raw:: html + +
  • +
+ +.. raw:: html + +
    +
  • + +I want to sort the titanic data according to the cabin class and age in descending order. + +.. ipython:: python + + titanic.sort_values(by=['Pclass', 'Age'], ascending=False).head() + +With :meth:`Series.sort_values`, the rows in the table are sorted according to the +defined column(s). The index will follow the row order. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +More details about sorting of tables is provided in the using guide section on :ref:`sorting data `. + +.. raw:: html + +
+ +Long to wide table format +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let’s use a small subset of the air quality data set. We focus on +:math:`NO_2` data and only use the first two measurements of each +location (i.e. the head of each group). The subset of data will be +called ``no2_subset`` + +.. ipython:: python + + # filter for no2 data only + no2 = air_quality[air_quality["parameter"] == "no2"] + +.. ipython:: python + + # use 2 measurements (head) for each location (groupby) + no2_subset = no2.sort_index().groupby(["location"]).head(2) + no2_subset + +.. image:: ../../_static/schemas/07_pivot.svg + :align: center + +.. raw:: html + +
    +
  • + +I want the values for the three stations as separate columns next to each other + +.. ipython:: python + + no2_subset.pivot(columns="location", values="value") + +The :meth:`~pandas.pivot_table` function is purely reshaping of the data: a single value +for each index/column combination is required. + +.. raw:: html + +
  • +
+ +As pandas support plotting of multiple columns (see :ref:`plotting tutorial <10min_tut_04_plotting>`) out of the box, the conversion from +*long* to *wide* table format enables the plotting of the different time +series at the same time: + +.. ipython:: python + + no2.head() + +.. ipython:: python + + @savefig 7_reshape_columns.png + no2.pivot(columns="location", values="value").plot() + +.. note:: + When the ``index`` parameter is not defined, the existing + index (row labels) is used. + +.. raw:: html + +
+ To user guide + +For more information about :meth:`~DataFrame.pivot`, see the user guide section on :ref:`pivoting DataFrame objects `. + +.. raw:: html + +
+ +Pivot table +~~~~~~~~~~~ + +.. image:: ../../_static/schemas/07_pivot_table.svg + :align: center + +.. raw:: html + +
    +
  • + +I want the mean concentrations for :math:`NO_2` and :math:`PM_{2.5}` in each of the stations in table form + +.. ipython:: python + + air_quality.pivot_table(values="value", index="location", + columns="parameter", aggfunc="mean") + +In the case of :meth:`~DataFrame.pivot`, the data is only rearranged. When multiple +values need to be aggregated (in this specific case, the values on +different time steps) :meth:`~DataFrame.pivot_table` can be used, providing an +aggregation function (e.g. mean) on how to combine these values. + +.. raw:: html + +
  • +
+ +Pivot table is a well known concept in spreadsheet software. When +interested in summary columns for each variable separately as well, put +the ``margin`` parameter to ``True``: + +.. ipython:: python + + air_quality.pivot_table(values="value", index="location", + columns="parameter", aggfunc="mean", + margins=True) + +.. raw:: html + +
+ To user guide + +For more information about :meth:`~DataFrame.pivot_table`, see the user guide section on :ref:`pivot tables `. + +.. raw:: html + +
+ +.. note:: + If case you are wondering, :meth:`~DataFrame.pivot_table` is indeed directly linked + to :meth:`~DataFrame.groupby`. The same result can be derived by grouping on both + ``parameter`` and ``location``: + + :: + + air_quality.groupby(["parameter", "location"]).mean() + +.. raw:: html + +
+ To user guide + +Have a look at :meth:`~DataFrame.groupby` in combination with :meth:`~DataFrame.unstack` at the user guide section on :ref:`combining stats and groupby `. + +.. raw:: html + +
+ +Wide to long format +~~~~~~~~~~~~~~~~~~~ + +Starting again from the wide format table created in the previous +section: + +.. ipython:: python + + no2_pivoted = no2.pivot(columns="location", values="value").reset_index() + no2_pivoted.head() + +.. image:: ../../_static/schemas/07_melt.svg + :align: center + +.. raw:: html + +
    +
  • + +I want to collect all air quality :math:`NO_2` measurements in a single column (long format) + +.. ipython:: python + + no_2 = no2_pivoted.melt(id_vars="date.utc") + no_2.head() + +The :func:`pandas.melt` method on a ``DataFrame`` converts the data table from wide +format to long format. The column headers become the variable names in a +newly created column. + +.. raw:: html + +
  • +
+ +The solution is the short version on how to apply :func:`pandas.melt`. The method +will *melt* all columns NOT mentioned in ``id_vars`` together into two +columns: A columns with the column header names and a column with the +values itself. The latter column gets by default the name ``value``. + +The :func:`pandas.melt` method can be defined in more detail: + +.. ipython:: python + + no_2 = no2_pivoted.melt(id_vars="date.utc", + value_vars=["BETR801", + "FR04014", + "London Westminster"], + value_name="NO_2", + var_name="id_location") + no_2.head() + +The result in the same, but in more detail defined: + +- ``value_vars`` defines explicitly which columns to *melt* together +- ``value_name`` provides a custom column name for the values column + instead of the default columns name ``value`` +- ``var_name`` provides a custom column name for the columns collecting + the column header names. Otherwise it takes the index name or a + default ``variable`` + +Hence, the arguments ``value_name`` and ``var_name`` are just +user-defined names for the two generated columns. The columns to melt +are defined by ``id_vars`` and ``value_vars``. + +.. raw:: html + +
+ To user guide + +Conversion from wide to long format with :func:`pandas.melt` is explained in the user guide section on :ref:`reshaping by melt `. + +.. raw:: html + +
+ +.. raw:: html + +
+

REMEMBER

+ +- Sorting by one or more columns is supported by ``sort_values`` +- The ``pivot`` function is purely restructering of the data, + ``pivot_table`` supports aggregations +- The reverse of ``pivot`` (long to wide format) is ``melt`` (wide to + long format) + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A full overview is available in the user guide on the pages about :ref:`reshaping and pivoting `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/08_combine_dataframes.rst b/doc/source/getting_started/intro_tutorials/08_combine_dataframes.rst new file mode 100644 index 0000000000000..f317e7a1f91b4 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/08_combine_dataframes.rst @@ -0,0 +1,326 @@ +.. _10min_tut_08_combine: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +For this tutorial, air quality data about :math:`NO_2` is used, made available by +`openaq `__ and downloaded using the +`py-openaq `__ package. + +The ``air_quality_no2_long.csv`` data set provides :math:`NO_2` +values for the measurement stations *FR04014*, *BETR801* and *London +Westminster* in respectively Paris, Antwerp and London. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + air_quality_no2 = pd.read_csv("data/air_quality_no2_long.csv", + parse_dates=True) + air_quality_no2 = air_quality_no2[["date.utc", "location", + "parameter", "value"]] + air_quality_no2.head() + +.. raw:: html + +
  • +
  • + +
    +
    +

    + +For this tutorial, air quality data about Particulate +matter less than 2.5 micrometers is used, made available by +`openaq `__ and downloaded using the +`py-openaq `__ package. + +The ``air_quality_pm25_long.csv`` data set provides :math:`PM_{25}` +values for the measurement stations *FR04014*, *BETR801* and *London +Westminster* in respectively Paris, Antwerp and London. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + air_quality_pm25 = pd.read_csv("data/air_quality_pm25_long.csv", + parse_dates=True) + air_quality_pm25 = air_quality_pm25[["date.utc", "location", + "parameter", "value"]] + air_quality_pm25.head() + +.. raw:: html + +
  • +
+
+ + +How to combine data from multiple tables? +----------------------------------------- + +Concatenating objects +~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/08_concat_row.svg + :align: center + +.. raw:: html + +
    +
  • + +I want to combine the measurements of :math:`NO_2` and :math:`PM_{25}`, two tables with a similar structure, in a single table + +.. ipython:: python + + air_quality = pd.concat([air_quality_pm25, air_quality_no2], axis=0) + air_quality.head() + +The :func:`~pandas.concat` function performs concatenation operations of multiple +tables along one of the axis (row-wise or column-wise). + +.. raw:: html + +
  • +
+ +By default concatenation is along axis 0, so the resulting table combines the rows +of the input tables. Let’s check the shape of the original and the +concatenated tables to verify the operation: + +.. ipython:: python + + print('Shape of the `air_quality_pm25` table: ', air_quality_pm25.shape) + print('Shape of the `air_quality_no2` table: ', air_quality_no2.shape) + print('Shape of the resulting `air_quality` table: ', air_quality.shape) + +Hence, the resulting table has 3178 = 1110 + 2068 rows. + +.. note:: + The **axis** argument will return in a number of pandas + methods that can be applied **along an axis**. A ``DataFrame`` has two + corresponding axes: the first running vertically downwards across rows + (axis 0), and the second running horizontally across columns (axis 1). + Most operations like concatenation or summary statistics are by default + across rows (axis 0), but can be applied across columns as well. + +Sorting the table on the datetime information illustrates also the +combination of both tables, with the ``parameter`` column defining the +origin of the table (either ``no2`` from table ``air_quality_no2`` or +``pm25`` from table ``air_quality_pm25``): + +.. ipython:: python + + air_quality = air_quality.sort_values("date.utc") + air_quality.head() + +In this specific example, the ``parameter`` column provided by the data +ensures that each of the original tables can be identified. This is not +always the case. the ``concat`` function provides a convenient solution +with the ``keys`` argument, adding an additional (hierarchical) row +index. For example: + +.. ipython:: python + + air_quality_ = pd.concat([air_quality_pm25, air_quality_no2], + keys=["PM25", "NO2"]) + +.. ipython:: python + + air_quality_.head() + +.. note:: + The existence of multiple row/column indices at the same time + has not been mentioned within these tutorials. *Hierarchical indexing* + or *MultiIndex* is an advanced and powerfull pandas feature to analyze + higher dimensional data. + + Multi-indexing is out of scope for this pandas introduction. For the + moment, remember that the function ``reset_index`` can be used to + convert any level of an index to a column, e.g. + ``air_quality.reset_index(level=0)`` + + .. raw:: html + +
+ To user guide + + Feel free to dive into the world of multi-indexing at the user guide section on :ref:`advanced indexing `. + + .. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +More options on table concatenation (row and column +wise) and how ``concat`` can be used to define the logic (union or +intersection) of the indexes on the other axes is provided at the section on +:ref:`object concatenation `. + +.. raw:: html + +
+ +Join tables using a common identifier +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/schemas/08_merge_left.svg + :align: center + +.. raw:: html + +
    +
  • + +Add the station coordinates, provided by the stations metadata table, to the corresponding rows in the measurements table. + +.. warning:: + The air quality measurement station coordinates are stored in a data + file ``air_quality_stations.csv``, downloaded using the + `py-openaq `__ package. + +.. ipython:: python + + stations_coord = pd.read_csv("data/air_quality_stations.csv") + stations_coord.head() + +.. note:: + The stations used in this example (FR04014, BETR801 and London + Westminster) are just three entries enlisted in the metadata table. We + only want to add the coordinates of these three to the measurements + table, each on the corresponding rows of the ``air_quality`` table. + +.. ipython:: python + + air_quality.head() + +.. ipython:: python + + air_quality = pd.merge(air_quality, stations_coord, + how='left', on='location') + air_quality.head() + +Using the :meth:`~pandas.merge` function, for each of the rows in the +``air_quality`` table, the corresponding coordinates are added from the +``air_quality_stations_coord`` table. Both tables have the column +``location`` in common which is used as a key to combine the +information. By choosing the ``left`` join, only the locations available +in the ``air_quality`` (left) table, i.e. FR04014, BETR801 and London +Westminster, end up in the resulting table. The ``merge`` function +supports multiple join options similar to database-style operations. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
    +
  • + +Add the parameter full description and name, provided by the parameters metadata table, to the measurements table + +.. warning:: + The air quality parameters metadata are stored in a data file + ``air_quality_parameters.csv``, downloaded using the + `py-openaq `__ package. + +.. ipython:: python + + air_quality_parameters = pd.read_csv("data/air_quality_parameters.csv") + air_quality_parameters.head() + +.. ipython:: python + + air_quality = pd.merge(air_quality, air_quality_parameters, + how='left', left_on='parameter', right_on='id') + air_quality.head() + +Compared to the previous example, there is no common column name. +However, the ``parameter`` column in the ``air_quality`` table and the +``id`` column in the ``air_quality_parameters_name`` both provide the +measured variable in a common format. The ``left_on`` and ``right_on`` +arguments are used here (instead of just ``on``) to make the link +between the two tables. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +pandas supports also inner, outer, and right joins. +More information on join/merge of tables is provided in the user guide section on +:ref:`database style merging of tables `. Or have a look at the +:ref:`comparison with SQL` page. + +.. raw:: html + +
+ +.. raw:: html + +
+

REMEMBER

+ +- Multiple tables can be concatenated both column as row wise using + the ``concat`` function. +- For database-like merging/joining of tables, use the ``merge`` + function. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +See the user guide for a full description of the various :ref:`facilities to combine data tables `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/09_timeseries.rst b/doc/source/getting_started/intro_tutorials/09_timeseries.rst new file mode 100644 index 0000000000000..d5b4b316130bb --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/09_timeseries.rst @@ -0,0 +1,389 @@ +.. _10min_tut_09_timeseries: + +{{ header }} + +.. ipython:: python + + import pandas as pd + import matplotlib.pyplot as plt + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +For this tutorial, air quality data about :math:`NO_2` and Particulate +matter less than 2.5 micrometers is used, made available by +`openaq `__ and downloaded using the +`py-openaq `__ package. +The ``air_quality_no2_long.csv"`` data set provides :math:`NO_2` values +for the measurement stations *FR04014*, *BETR801* and *London +Westminster* in respectively Paris, Antwerp and London. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + air_quality = pd.read_csv("data/air_quality_no2_long.csv") + air_quality = air_quality.rename(columns={"date.utc": "datetime"}) + air_quality.head() + +.. ipython:: python + + air_quality.city.unique() + +.. raw:: html + +
  • +
+
+ +How to handle time series data with ease? +----------------------------------------- + +Using pandas datetime properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. raw:: html + +
    +
  • + +I want to work with the dates in the column ``datetime`` as datetime objects instead of plain text + +.. ipython:: python + + air_quality["datetime"] = pd.to_datetime(air_quality["datetime"]) + air_quality["datetime"] + +Initially, the values in ``datetime`` are character strings and do not +provide any datetime operations (e.g. extract the year, day of the +week,…). By applying the ``to_datetime`` function, pandas interprets the +strings and convert these to datetime (i.e. ``datetime64[ns, UTC]``) +objects. In pandas we call these datetime objects similar to +``datetime.datetime`` from the standard library a :class:`pandas.Timestamp`. + +.. raw:: html + +
  • +
+ +.. note:: + As many data sets do contain datetime information in one of + the columns, pandas input function like :func:`pandas.read_csv` and :func:`pandas.read_json` + can do the transformation to dates when reading the data using the + ``parse_dates`` parameter with a list of the columns to read as + Timestamp: + + :: + + pd.read_csv("../data/air_quality_no2_long.csv", parse_dates=["datetime"]) + +Why are these :class:`pandas.Timestamp` objects useful. Let’s illustrate the added +value with some example cases. + + What is the start and end date of the time series data set working + with? + +.. ipython:: python + + air_quality["datetime"].min(), air_quality["datetime"].max() + +Using :class:`pandas.Timestamp` for datetimes enable us to calculate with date +information and make them comparable. Hence, we can use this to get the +length of our time series: + +.. ipython:: python + + air_quality["datetime"].max() - air_quality["datetime"].min() + +The result is a :class:`pandas.Timedelta` object, similar to ``datetime.timedelta`` +from the standard Python library and defining a time duration. + +.. raw:: html + +
+ To user guide + +The different time concepts supported by pandas are explained in the user guide section on :ref:`time related concepts `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +I want to add a new column to the ``DataFrame`` containing only the month of the measurement + +.. ipython:: python + + air_quality["month"] = air_quality["datetime"].dt.month + air_quality.head() + +By using ``Timestamp`` objects for dates, a lot of time-related +properties are provided by pandas. For example the ``month``, but also +``year``, ``weekofyear``, ``quarter``,… All of these properties are +accessible by the ``dt`` accessor. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +An overview of the existing date properties is given in the +:ref:`time and date components overview table `. More details about the ``dt`` accessor +to return datetime like properties is explained in a dedicated section on the :ref:`dt accessor `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +What is the average :math:`NO_2` concentration for each day of the week for each of the measurement locations? + +.. ipython:: python + + air_quality.groupby( + [air_quality["datetime"].dt.weekday, "location"])["value"].mean() + +Remember the split-apply-combine pattern provided by ``groupby`` from the +:ref:`tutorial on statistics calculation <10min_tut_06_stats>`? +Here, we want to calculate a given statistic (e.g. mean :math:`NO_2`) +**for each weekday** and **for each measurement location**. To group on +weekdays, we use the datetime property ``weekday`` (with Monday=0 and +Sunday=6) of pandas ``Timestamp``, which is also accessible by the +``dt`` accessor. The grouping on both locations and weekdays can be done +to split the calculation of the mean on each of these combinations. + +.. danger:: + As we are working with a very short time series in these + examples, the analysis does not provide a long-term representative + result! + +.. raw:: html + +
  • +
+ +.. raw:: html + +
    +
  • + +Plot the typical :math:`NO_2` pattern during the day of our time series of all stations together. In other words, what is the average value for each hour of the day? + +.. ipython:: python + + fig, axs = plt.subplots(figsize=(12, 4)) + air_quality.groupby( + air_quality["datetime"].dt.hour)["value"].mean().plot(kind='bar', + rot=0, + ax=axs) + plt.xlabel("Hour of the day"); # custom x label using matplotlib + @savefig 09_bar_chart.png + plt.ylabel("$NO_2 (µg/m^3)$"); + +Similar to the previous case, we want to calculate a given statistic +(e.g. mean :math:`NO_2`) **for each hour of the day** and we can use the +split-apply-combine approach again. For this case, the datetime property ``hour`` +of pandas ``Timestamp``, which is also accessible by the ``dt`` accessor. + +.. raw:: html + +
  • +
+ +Datetime as index +~~~~~~~~~~~~~~~~~ + +In the :ref:`tutorial on reshaping <10min_tut_07_reshape>`, +:meth:`~pandas.pivot` was introduced to reshape the data table with each of the +measurements locations as a separate column: + +.. ipython:: python + + no_2 = air_quality.pivot(index="datetime", columns="location", values="value") + no_2.head() + +.. note:: + By pivoting the data, the datetime information became the + index of the table. In general, setting a column as an index can be + achieved by the ``set_index`` function. + +Working with a datetime index (i.e. ``DatetimeIndex``) provides powerful +functionalities. For example, we do not need the ``dt`` accessor to get +the time series properties, but have these properties available on the +index directly: + +.. ipython:: python + + no_2.index.year, no_2.index.weekday + +Some other advantages are the convenient subsetting of time period or +the adapted time scale on plots. Let’s apply this on our data. + +.. raw:: html + +
    +
  • + +Create a plot of the :math:`NO_2` values in the different stations from the 20th of May till the end of 21st of May + +.. ipython:: python + :okwarning: + + @savefig 09_time_section.png + no_2["2019-05-20":"2019-05-21"].plot(); + +By providing a **string that parses to a datetime**, a specific subset of the data can be selected on a ``DatetimeIndex``. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +More information on the ``DatetimeIndex`` and the slicing by using strings is provided in the section on :ref:`time series indexing `. + +.. raw:: html + +
+ +Resample a time series to another frequency +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. raw:: html + +
    +
  • + +Aggregate the current hourly time series values to the monthly maximum value in each of the stations. + +.. ipython:: python + + monthly_max = no_2.resample("M").max() + monthly_max + +A very powerful method on time series data with a datetime index, is the +ability to :meth:`~Series.resample` time series to another frequency (e.g., +converting secondly data into 5-minutely data). + +.. raw:: html + +
  • +
+ +The :meth:`~Series.resample` method is similar to a groupby operation: + +- it provides a time-based grouping, by using a string (e.g. ``M``, + ``5H``,…) that defines the target frequency +- it requires an aggregation function such as ``mean``, ``max``,… + +.. raw:: html + +
+ To user guide + +An overview of the aliases used to define time series frequencies is given in the :ref:`offset aliases overview table `. + +.. raw:: html + +
+ +When defined, the frequency of the time series is provided by the +``freq`` attribute: + +.. ipython:: python + + monthly_max.index.freq + +.. raw:: html + +
    +
  • + +Make a plot of the daily median :math:`NO_2` value in each of the stations. + +.. ipython:: python + :okwarning: + + @savefig 09_resample_mean.png + no_2.resample("D").mean().plot(style="-o", figsize=(10, 5)); + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +More details on the power of time series ``resampling`` is provided in the user gudie section on :ref:`resampling `. + +.. raw:: html + +
+ +.. raw:: html + +
+

REMEMBER

+ +- Valid date strings can be converted to datetime objects using + ``to_datetime`` function or as part of read functions. +- Datetime objects in pandas supports calculations, logical operations + and convenient date-related properties using the ``dt`` accessor. +- A ``DatetimeIndex`` contains these date-related properties and + supports convenient slicing. +- ``Resample`` is a powerful method to change the frequency of a time + series. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A full overview on time series is given in the pages on :ref:`time series and date functionality `. + +.. raw:: html + +
\ No newline at end of file diff --git a/doc/source/getting_started/intro_tutorials/10_text_data.rst b/doc/source/getting_started/intro_tutorials/10_text_data.rst new file mode 100644 index 0000000000000..3ff64875d807b --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/10_text_data.rst @@ -0,0 +1,278 @@ +.. _10min_tut_10_text: + +{{ header }} + +.. ipython:: python + + import pandas as pd + +.. raw:: html + +
+
+
+ Data used for this tutorial: +
+
+
    +
  • + +
    +
    +

    + +This tutorial uses the titanic data set, stored as CSV. The data +consists of the following data columns: + +- PassengerId: Id of every passenger. +- Survived: This feature have value 0 and 1. 0 for not survived and 1 + for survived. +- Pclass: There are 3 classes: Class 1, Class 2 and Class 3. +- Name: Name of passenger. +- Sex: Gender of passenger. +- Age: Age of passenger. +- SibSp: Indication that passenger have siblings and spouse. +- Parch: Whether a passenger is alone or have family. +- Ticket: Ticket number of passenger. +- Fare: Indicating the fare. +- Cabin: The cabin of passenger. +- Embarked: The embarked category. + +.. raw:: html + +

    + To raw data +
    +
    + +.. ipython:: python + + titanic = pd.read_csv("data/titanic.csv") + titanic.head() + +.. raw:: html + +
  • +
+
+ +How to manipulate textual data? +------------------------------- + +.. raw:: html + +
    +
  • + +Make all name characters lowercase + +.. ipython:: python + + titanic["Name"].str.lower() + +To make each of the strings in the ``Name`` column lowercase, select the ``Name`` column +(see :ref:`tutorial on selection of data <10min_tut_03_subset>`), add the ``str`` accessor and +apply the ``lower`` method. As such, each of the strings is converted element wise. + +.. raw:: html + +
  • +
+ +Similar to datetime objects in the :ref:`time series tutorial <10min_tut_09_timeseries>` +having a ``dt`` accessor, a number of +specialized string methods are available when using the ``str`` +accessor. These methods have in general matching names with the +equivalent built-in string methods for single elements, but are applied +element-wise (remember :ref:`element wise calculations <10min_tut_05_columns>`?) +on each of the values of the columns. + +.. raw:: html + +
    +
  • + +Create a new column ``Surname`` that contains the surname of the Passengers by extracting the part before the comma. + +.. ipython:: python + + titanic["Name"].str.split(",") + +Using the :meth:`Series.str.split` method, each of the values is returned as a list of +2 elements. The first element is the part before the comma and the +second element the part after the comma. + +.. ipython:: python + + titanic["Surname"] = titanic["Name"].str.split(",").str.get(0) + titanic["Surname"] + +As we are only interested in the first part representing the surname +(element 0), we can again use the ``str`` accessor and apply :meth:`Series.str.get` to +extract the relevant part. Indeed, these string functions can be +concatenated to combine multiple functions at once! + +.. raw:: html + +
  • +
+ +.. raw:: html + +
+ To user guide + +More information on extracting parts of strings is available in the user guide section on :ref:`splitting and replacing strings `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +Extract the passenger data about the Countess on board of the Titanic. + +.. ipython:: python + + titanic["Name"].str.contains("Countess") + +.. ipython:: python + + titanic[titanic["Name"].str.contains("Countess")] + +(*Interested in her story? See*\ `Wikipedia `__\ *!*) + +The string method :meth:`Series.str.contains` checks for each of the values in the +column ``Name`` if the string contains the word ``Countess`` and returns +for each of the values ``True`` (``Countess`` is part of the name) of +``False`` (``Countess`` is notpart of the name). This output can be used +to subselect the data using conditional (boolean) indexing introduced in +the :ref:`subsetting of data tutorial <10min_tut_03_subset>`. As there was +only 1 Countess on the Titanic, we get one row as a result. + +.. raw:: html + +
  • +
+ +.. note:: + More powerful extractions on strings is supported, as the + :meth:`Series.str.contains` and :meth:`Series.str.extract` methods accepts `regular + expressions `__, but out of + scope of this tutorial. + +.. raw:: html + +
+ To user guide + +More information on extracting parts of strings is available in the user guide section on :ref:`string matching and extracting `. + +.. raw:: html + +
+ +.. raw:: html + +
    +
  • + +Which passenger of the titanic has the longest name? + +.. ipython:: python + + titanic["Name"].str.len() + +To get the longest name we first have to get the lenghts of each of the +names in the ``Name`` column. By using pandas string methods, the +:meth:`Series.str.len` function is applied to each of the names individually +(element-wise). + +.. ipython:: python + + titanic["Name"].str.len().idxmax() + +Next, we need to get the corresponding location, preferably the index +label, in the table for which the name length is the largest. The +:meth:`~Series.idxmax`` method does exactly that. It is not a string method and is +applied to integers, so no ``str`` is used. + +.. ipython:: python + + titanic.loc[titanic["Name"].str.len().idxmax(), "Name"] + +Based on the index name of the row (``307``) and the column (``Name``), +we can do a selection using the ``loc`` operator, introduced in the +`tutorial on subsetting <3_subset_data.ipynb>`__. + +.. raw:: html + +
  • +
+ +.. raw:: html + +
    +
  • + +In the ‘Sex’ columns, replace values of ‘male’ by ‘M’ and all ‘female’ values by ‘F’ + +.. ipython:: python + + titanic["Sex_short"] = titanic["Sex"].replace({"male": "M", + "female": "F"}) + titanic["Sex_short"] + +Whereas :meth:`~Series.replace` is not a string method, it provides a convenient way +to use mappings or vocabularies to translate certain values. It requires +a ``dictionary`` to define the mapping ``{from : to}``. + +.. raw:: html + +
  • +
+ +.. warning:: + There is also a :meth:`~Series.str.replace` methods available to replace a + specific set of characters. However, when having a mapping of multiple + values, this would become: + + :: + + titanic["Sex_short"] = titanic["Sex"].str.replace("female", "F") + titanic["Sex_short"] = titanic["Sex_short"].str.replace("male", "M") + + This would become cumbersome and easily lead to mistakes. Just think (or + try out yourself) what would happen if those two statements are applied + in the opposite order… + +.. raw:: html + +
+

REMEMBER

+ +- String methods are available using the ``str`` accessor. +- String methods work element wise and can be used for conditional + indexing. +- The ``replace`` method is a convenient method to convert values + according to a given dictionary. + +.. raw:: html + +
+ +.. raw:: html + +
+ To user guide + +A full overview is provided in the user guide pages on :ref:`working with text data `. + +.. raw:: html + +
diff --git a/doc/source/getting_started/intro_tutorials/index.rst b/doc/source/getting_started/intro_tutorials/index.rst new file mode 100644 index 0000000000000..28e7610866461 --- /dev/null +++ b/doc/source/getting_started/intro_tutorials/index.rst @@ -0,0 +1,22 @@ +{{ header }} + +.. _10times1minute: + +========================= +Getting started tutorials +========================= + +.. toctree:: + :maxdepth: 1 + + 01_table_oriented + 02_read_write + 03_subset_data + 04_plotting + 05_add_columns + 06_calculate_statistics + 07_reshape_table_layout + 08_combine_dataframes + 09_timeseries + 10_text_data + diff --git a/doc/source/index.rst.template b/doc/source/index.rst.template index 5690bb2e4a875..7eb25790f6a7a 100644 --- a/doc/source/index.rst.template +++ b/doc/source/index.rst.template @@ -104,6 +104,7 @@ programming language. {% if single_doc and single_doc.endswith('.rst') -%} .. toctree:: :maxdepth: 3 + :titlesonly: {{ single_doc[:-4] }} {% elif single_doc %} @@ -115,6 +116,7 @@ programming language. .. toctree:: :maxdepth: 3 :hidden: + :titlesonly: {% endif %} {% if not single_doc %} What's New in 1.1.0 diff --git a/doc/source/reference/frame.rst b/doc/source/reference/frame.rst index c7b1cc1c832be..b326bbb5a465e 100644 --- a/doc/source/reference/frame.rst +++ b/doc/source/reference/frame.rst @@ -170,6 +170,7 @@ Computations / descriptive stats DataFrame.std DataFrame.var DataFrame.nunique + DataFrame.value_counts Reindexing / selection / label manipulation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/user_guide/boolean.rst b/doc/source/user_guide/boolean.rst index 5276bc6142206..6370a523b9a0d 100644 --- a/doc/source/user_guide/boolean.rst +++ b/doc/source/user_guide/boolean.rst @@ -9,7 +9,7 @@ .. _boolean: ************************** -Nullable Boolean Data Type +Nullable Boolean data type ************************** .. versionadded:: 1.0.0 @@ -20,8 +20,9 @@ Nullable Boolean Data Type Indexing with NA values ----------------------- -pandas does not allow indexing with NA values. Attempting to do so -will raise a ``ValueError``. +pandas allows indexing with ``NA`` values in a boolean array, which are treated as ``False``. + +.. versionchanged:: 1.0.2 .. ipython:: python :okexcept: @@ -30,12 +31,11 @@ will raise a ``ValueError``. mask = pd.array([True, False, pd.NA], dtype="boolean") s[mask] -The missing values will need to be explicitly filled with True or False prior -to using the array as a mask. +If you would prefer to keep the ``NA`` values you can manually fill them with ``fillna(True)``. .. ipython:: python - s[mask.fillna(False)] + s[mask.fillna(True)] .. _boolean.kleene: diff --git a/doc/source/user_guide/indexing.rst b/doc/source/user_guide/indexing.rst index a8cdf4a61073d..2bd3ff626f2e1 100644 --- a/doc/source/user_guide/indexing.rst +++ b/doc/source/user_guide/indexing.rst @@ -59,7 +59,7 @@ of multi-axis indexing. slices, **both** the start and the stop are included, when present in the index! See :ref:`Slicing with labels ` and :ref:`Endpoints are inclusive `.) - * A boolean array + * A boolean array (any ``NA`` values will be treated as ``False``). * A ``callable`` function with one argument (the calling Series or DataFrame) and that returns valid output for indexing (one of the above). @@ -75,7 +75,7 @@ of multi-axis indexing. * An integer e.g. ``5``. * A list or array of integers ``[4, 3, 0]``. * A slice object with ints ``1:7``. - * A boolean array. + * A boolean array (any ``NA`` values will be treated as ``False``). * A ``callable`` function with one argument (the calling Series or DataFrame) and that returns valid output for indexing (one of the above). @@ -374,6 +374,14 @@ For getting values with a boolean array: df1.loc['a'] > 0 df1.loc[:, df1.loc['a'] > 0] +NA values in a boolean array propogate as ``False``: + +.. versionchanged:: 1.0.2 + + mask = pd.array([True, False, True, False, pd.NA, False], dtype="boolean") + mask + df1[mask] + For getting a value explicitly: .. ipython:: python diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index bd19b35e8d9e8..c34247a49335d 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -3815,7 +3815,7 @@ The right-hand side of the sub-expression (after a comparison operator) can be: .. code-block:: ipython string = "HolyMoly'" - store.select('df', 'index == %s' % string) + store.select('df', f'index == {string}') The latter will **not** work and will raise a ``SyntaxError``.Note that there's a single quote followed by a double quote in the ``string`` diff --git a/doc/source/user_guide/merging.rst b/doc/source/user_guide/merging.rst index 8fdcd8d281a41..8302b5c5dea60 100644 --- a/doc/source/user_guide/merging.rst +++ b/doc/source/user_guide/merging.rst @@ -724,6 +724,27 @@ either the left or right tables, the values in the joined table will be labels=['left', 'right'], vertical=False); plt.close('all'); +You can merge a mult-indexed Series and a DataFrame, if the names of +the MultiIndex correspond to the columns from the DataFrame. Transform +the Series to a DataFrame using :meth:`Series.reset_index` before merging, +as shown in the following example. + +.. ipython:: python + + df = pd.DataFrame({"Let": ["A", "B", "C"], "Num": [1, 2, 3]}) + df + + ser = pd.Series( + ["a", "b", "c", "d", "e", "f"], + index=pd.MultiIndex.from_arrays( + [["A", "B", "C"] * 2, [1, 2, 3, 4, 5, 6]], names=["Let", "Num"] + ), + ) + ser + + result = pd.merge(df, ser.reset_index(), on=['Let', 'Num']) + + Here is another example with duplicate join keys in DataFrames: .. ipython:: python diff --git a/doc/source/user_guide/reshaping.rst b/doc/source/user_guide/reshaping.rst index b28354cd8b5f2..58733b852e3a1 100644 --- a/doc/source/user_guide/reshaping.rst +++ b/doc/source/user_guide/reshaping.rst @@ -6,6 +6,8 @@ Reshaping and pivot tables ************************** +.. _reshaping.reshaping: + Reshaping by pivoting DataFrame objects --------------------------------------- @@ -15,7 +17,6 @@ Reshaping by pivoting DataFrame objects :suppress: import pandas._testing as tm - tm.N = 3 def unpivot(frame): N, K = frame.shape @@ -25,7 +26,7 @@ Reshaping by pivoting DataFrame objects columns = ['date', 'variable', 'value'] return pd.DataFrame(data, columns=columns) - df = unpivot(tm.makeTimeDataFrame()) + df = unpivot(tm.makeTimeDataFrame(3)) Data is often stored in so-called "stacked" or "record" format: @@ -40,9 +41,6 @@ For the curious here is how the above ``DataFrame`` was created: import pandas._testing as tm - tm.N = 3 - - def unpivot(frame): N, K = frame.shape data = {'value': frame.to_numpy().ravel('F'), @@ -51,7 +49,7 @@ For the curious here is how the above ``DataFrame`` was created: return pd.DataFrame(data, columns=['date', 'variable', 'value']) - df = unpivot(tm.makeTimeDataFrame()) + df = unpivot(tm.makeTimeDataFrame(3)) To select out everything for variable ``A`` we could do: @@ -314,6 +312,8 @@ user-friendly. dft pd.wide_to_long(dft, ["A", "B"], i="id", j="year") +.. _reshaping.combine_with_groupby: + Combining with stats and GroupBy -------------------------------- diff --git a/doc/source/user_guide/text.rst b/doc/source/user_guide/text.rst index 88c86ac212f11..2e4d0fecaf5cf 100644 --- a/doc/source/user_guide/text.rst +++ b/doc/source/user_guide/text.rst @@ -189,12 +189,11 @@ and replacing any remaining whitespaces with underscores: Generally speaking, the ``.str`` accessor is intended to work only on strings. With very few exceptions, other uses are not supported, and may be disabled at a later point. +.. _text.split: Splitting and replacing strings ------------------------------- -.. _text.split: - Methods like ``split`` return a Series of lists: .. ipython:: python diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index 3fdab0fd26643..f208c8d576131 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -2297,6 +2297,35 @@ To remove time zone information, use ``tz_localize(None)`` or ``tz_convert(None) # tz_convert(None) is identical to tz_convert('UTC').tz_localize(None) didx.tz_convert('UTC').tz_localize(None) +.. _timeseries.fold: + +Fold +~~~~ + +.. versionadded:: 1.1.0 + +For ambiguous times, pandas supports explicitly specifying the keyword-only fold argument. +Due to daylight saving time, one wall clock time can occur twice when shifting +from summer to winter time; fold describes whether the datetime-like corresponds +to the first (0) or the second time (1) the wall clock hits the ambiguous time. +Fold is supported only for constructing from naive ``datetime.datetime`` +(see `datetime documentation `__ for details) or from :class:`Timestamp` +or for constructing from components (see below). Only ``dateutil`` timezones are supported +(see `dateutil documentation `__ +for ``dateutil`` methods that deal with ambiguous datetimes) as ``pytz`` +timezones do not support fold (see `pytz documentation `__ +for details on how ``pytz`` deals with ambiguous datetimes). To localize an ambiguous datetime +with ``pytz``, please use :meth:`Timestamp.tz_localize`. In general, we recommend to rely +on :meth:`Timestamp.tz_localize` when localizing ambiguous datetimes if you need direct +control over how they are handled. + +.. ipython:: python + + pd.Timestamp(datetime.datetime(2019, 10, 27, 1, 30, 0, 0), + tz='dateutil/Europe/London', fold=0) + pd.Timestamp(year=2019, month=10, day=27, hour=1, minute=30, + tz='dateutil/Europe/London', fold=1) + .. _timeseries.timezone_ambiguous: Ambiguous times when localizing diff --git a/doc/source/whatsnew/index.rst b/doc/source/whatsnew/index.rst index 68aabfe76d8de..cbfeb0352c283 100644 --- a/doc/source/whatsnew/index.rst +++ b/doc/source/whatsnew/index.rst @@ -7,7 +7,7 @@ Release Notes ************* This is the list of changes to pandas between each release. For full details, -see the commit logs at https://github.com/pandas-dev/pandas. For install and +see the `commit logs `_. For install and upgrade instructions, see :ref:`install`. Version 1.1 @@ -24,9 +24,9 @@ Version 1.0 .. toctree:: :maxdepth: 2 - v1.0.0 - v1.0.1 v1.0.2 + v1.0.1 + v1.0.0 Version 0.25 ------------ diff --git a/doc/source/whatsnew/v1.0.2.rst b/doc/source/whatsnew/v1.0.2.rst index 70aaaa6d0a60d..1b6098e6b6ac1 100644 --- a/doc/source/whatsnew/v1.0.2.rst +++ b/doc/source/whatsnew/v1.0.2.rst @@ -16,18 +16,72 @@ Fixed regressions ~~~~~~~~~~~~~~~~~ - Fixed regression in :meth:`DataFrame.to_excel` when ``columns`` kwarg is passed (:issue:`31677`) +- Fixed regression in :meth:`Series.align` when ``other`` is a DataFrame and ``method`` is not None (:issue:`31785`) +- Fixed regression in :meth:`pandas.core.groupby.RollingGroupby.apply` where the ``raw`` parameter was ignored (:issue:`31754`) +- Fixed regression in :meth:`rolling(..).corr() ` when using a time offset (:issue:`31789`) +- Fixed regression in :meth:`DataFrameGroupBy.nunique` which was modifying the original values if ``NaN`` values were present (:issue:`31950`) +- Fixed regression where :func:`read_pickle` raised a ``UnicodeDecodeError`` when reading a py27 pickle with :class:`MultiIndex` column (:issue:`31988`). +- Fixed regression in :class:`DataFrame` arithmetic operations with mis-matched columns (:issue:`31623`) +- Fixed regression in :meth:`GroupBy.agg` calling a user-provided function an extra time on an empty input (:issue:`31760`) +- Joining on :class:`DatetimeIndex` or :class:`TimedeltaIndex` will preserve ``freq`` in simple cases (:issue:`32166`) - .. --------------------------------------------------------------------------- +Indexing with Nullable Boolean Arrays +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously indexing with a nullable Boolean array containing ``NA`` would raise a ``ValueError``, however this is now permitted with ``NA`` being treated as ``False``. (:issue:`31503`) + +.. ipython:: python + + s = pd.Series([1, 2, 3, 4]) + mask = pd.array([True, True, False, None], dtype="boolean") + s + mask + +*pandas 1.0.0-1.0.1* + +.. code-block:: python + + >>> s[mask] + Traceback (most recent call last): + ... + ValueError: cannot mask with array containing NA / NaN values + +*pandas 1.0.2* + +.. ipython:: python + + s[mask] + .. _whatsnew_102.bug_fixes: Bug fixes ~~~~~~~~~ +**Datetimelike** + +- Bug in :meth:`DataFrame.reindex` and :meth:`Series.reindex` when reindexing with a tz-aware index (:issue:`26683`) +- Bug where :func:`to_datetime` would raise when passed ``pd.NA`` (:issue:`32213`) + +**Categorical** + +- Fixed bug where :meth:`Categorical.from_codes` improperly raised a ``ValueError`` when passed nullable integer codes. (:issue:`31779`) +- Fixed bug where :meth:`Categorical` constructor would raise a ``TypeError`` when given a numpy array containing ``pd.NA``. (:issue:`31927`) +- Bug in :class:`Categorical` that would ignore or crash when calling :meth:`Series.replace` with a list-like ``to_replace`` (:issue:`31720`) + **I/O** - Using ``pd.NA`` with :meth:`DataFrame.to_json` now correctly outputs a null value instead of an empty object (:issue:`31615`) +- Fixed bug in parquet roundtrip with nullable unsigned integer dtypes (:issue:`31896`). + +**Experimental dtypes** + +- Fix bug in :meth:`DataFrame.convert_dtypes` for columns that were already using the ``"string"`` dtype (:issue:`31731`). +- Fixed bug in setting values using a slice indexer with string dtype (:issue:`31772`) +- Fixed bug where :meth:`GroupBy.first` and :meth:`GroupBy.last` would raise a ``TypeError`` when groups contained ``pd.NA`` in a column of object dtype (:issue:`32123`) +- Fix bug in :meth:`Series.convert_dtypes` for series with mix of integers and strings (:issue:`32117`) .. --------------------------------------------------------------------------- @@ -36,4 +90,4 @@ Bug fixes Contributors ~~~~~~~~~~~~ -.. contributors:: v1.0.1..v1.0.2|HEAD \ No newline at end of file +.. contributors:: v1.0.1..v1.0.2|HEAD diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index aea5695a96388..0f18a1fd81815 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -36,13 +36,36 @@ For example: ser["2014"] ser.loc["May 2015"] +.. _whatsnew_110.timestamp_fold_support: + +Fold argument support in Timestamp constructor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:class:`Timestamp:` now supports the keyword-only fold argument according to `PEP 495 `_ similar to parent ``datetime.datetime`` class. It supports both accepting fold as an initialization argument and inferring fold from other constructor arguments (:issue:`25057`, :issue:`31338`). Support is limited to ``dateutil`` timezones as ``pytz`` doesn't support fold. + +For example: + +.. ipython:: python + + ts = pd.Timestamp("2019-10-27 01:30:00+00:00") + ts.fold + +.. ipython:: python + + ts = pd.Timestamp(year=2019, month=10, day=27, hour=1, minute=30, + tz="dateutil/Europe/London", fold=1) + ts + +For more on working with fold, see :ref:`Fold subsection ` in the user guide. + .. _whatsnew_110.enhancements.other: Other enhancements ^^^^^^^^^^^^^^^^^^ - :class:`Styler` may now render CSS more efficiently where multiple cells have the same styling (:issue:`30876`) -- +- When writing directly to a sqlite connection :func:`to_sql` now supports the ``multi`` method (:issue:`29921`) +- `OptionError` is now exposed in `pandas.errors` (:issue:`27553`) - .. --------------------------------------------------------------------------- @@ -54,7 +77,10 @@ Other API changes - :meth:`Series.describe` will now show distribution percentiles for ``datetime`` dtypes, statistics ``first`` and ``last`` will now be ``min`` and ``max`` to match with numeric dtypes in :meth:`DataFrame.describe` (:issue:`30164`) +- Added :meth:`DataFrame.value_counts` (:issue:`5377`) - :meth:`Groupby.groups` now returns an abbreviated representation when called on large dataframes (:issue:`1135`) +- ``loc`` lookups with an object-dtype :class:`Index` and an integer key will now raise ``KeyError`` instead of ``TypeError`` when key is missing (:issue:`31905`) +- Backwards incompatible API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -64,7 +90,76 @@ Backwards incompatible API changes now raise a ``TypeError`` if a not-accepted keyword argument is passed into it. Previously a ``UnsupportedFunctionCall`` was raised (``AssertionError`` if ``min_count`` passed into :meth:`~DataFrameGroupby.median``) (:issue:`31485`) - :meth:`DataFrame.at` and :meth:`Series.at` will raise a ``TypeError`` instead of a ``ValueError`` if an incompatible key is passed, and ``KeyError`` if a missing key is passed, matching the behavior of ``.loc[]`` (:issue:`31722`) -- + +.. _whatsnew_110.api_breaking.indexing_raises_key_errors: + +Failed Label-Based Lookups Always Raise KeyError +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Label lookups ``series[key]``, ``series.loc[key]`` and ``frame.loc[key]`` +used to raises either ``KeyError`` or ``TypeError`` depending on the type of +key and type of :class:`Index`. These now consistently raise ``KeyError`` (:issue:`31867`) + +.. ipython:: python + + ser1 = pd.Series(range(3), index=[0, 1, 2]) + ser2 = pd.Series(range(3), index=pd.date_range("2020-02-01", periods=3)) + +*Previous behavior*: + +.. code-block:: ipython + + In [3]: ser1[1.5] + ... + TypeError: cannot do label indexing on Int64Index with these indexers [1.5] of type float + + In [4] ser1["foo"] + ... + KeyError: 'foo' + + In [5]: ser1.loc[1.5] + ... + TypeError: cannot do label indexing on Int64Index with these indexers [1.5] of type float + + In [6]: ser1.loc["foo"] + ... + KeyError: 'foo' + + In [7]: ser2.loc[1] + ... + TypeError: cannot do label indexing on DatetimeIndex with these indexers [1] of type int + + In [8]: ser2.loc[pd.Timestamp(0)] + ... + KeyError: Timestamp('1970-01-01 00:00:00') + +*New behavior*: + +.. code-block:: ipython + + In [3]: ser1[1.5] + ... + KeyError: 1.5 + + In [4] ser1["foo"] + ... + KeyError: 'foo' + + In [5]: ser1.loc[1.5] + ... + KeyError: 1.5 + + In [6]: ser1.loc["foo"] + ... + KeyError: 'foo' + + In [7]: ser2.loc[1] + ... + KeyError: 1 + + In [8]: ser2.loc[pd.Timestamp(0)] + ... + KeyError: Timestamp('1970-01-01 00:00:00') .. --------------------------------------------------------------------------- @@ -72,7 +167,8 @@ Backwards incompatible API changes Deprecations ~~~~~~~~~~~~ - +- Lookups on a :class:`Series` with a single-item list containing a slice (e.g. ``ser[[slice(0, 4)]]``) are deprecated, will raise in a future version. Either convert the list to tuple, or pass the slice directly instead (:issue:`31333`) +- :meth:`DataFrame.mean` and :meth:`DataFrame.median` with ``numeric_only=None`` will include datetime64 and datetime64tz columns in a future version (:issue:`29941`) - - @@ -100,7 +196,8 @@ Bug fixes Categorical ^^^^^^^^^^^ -- +- Bug where :func:`merge` was unable to join on non-unique categorical indices (:issue:`28189`) +- Bug when passing categorical data to :class:`Index` constructor along with ``dtype=object`` incorrectly returning a :class:`CategoricalIndex` instead of object-dtype :class:`Index` (:issue:`32167`) - Datetimelike @@ -109,8 +206,8 @@ Datetimelike - Bug in :class:`Timestamp` where constructing :class:`Timestamp` from ambiguous epoch time and calling constructor again changed :meth:`Timestamp.value` property (:issue:`24329`) - :meth:`DatetimeArray.searchsorted`, :meth:`TimedeltaArray.searchsorted`, :meth:`PeriodArray.searchsorted` not recognizing non-pandas scalars and incorrectly raising ``ValueError`` instead of ``TypeError`` (:issue:`30950`) - Bug in :class:`Timestamp` where constructing :class:`Timestamp` with dateutil timezone less than 128 nanoseconds before daylight saving time switch from winter to summer would result in nonexistent time (:issue:`31043`) -- Bug in :meth:`DataFrame.reindex` and :meth:`Series.reindex` when reindexing with a tz-aware index (:issue:`26683`) - Bug in :meth:`Period.to_timestamp`, :meth:`Period.start_time` with microsecond frequency returning a timestamp one nanosecond earlier than the correct time (:issue:`31475`) +- :class:`Timestamp` raising confusing error message when year, month or day is missing (:issue:`31200`) Timedelta ^^^^^^^^^ @@ -158,6 +255,9 @@ Indexing - Bug in :meth:`PeriodIndex.is_monotonic` incorrectly returning ``True`` when containing leading ``NaT`` entries (:issue:`31437`) - Bug in :meth:`DatetimeIndex.get_loc` raising ``KeyError`` with converted-integer key instead of the user-passed key (:issue:`31425`) - Bug in :meth:`Series.xs` incorrectly returning ``Timestamp`` instead of ``datetime64`` in some object-dtype cases (:issue:`31630`) +- Bug in :meth:`DataFrame.iat` incorrectly returning ``Timestamp`` instead of ``datetime`` in some object-dtype cases (:issue:`32809`) +- Bug in :meth:`Series.loc` and :meth:`DataFrame.loc` when indexing with an integer key on a object-dtype :class:`Index` that is not all-integers (:issue:`31905`) +- Missing ^^^^^^^ @@ -175,6 +275,16 @@ MultiIndex index=[["a", "a", "b", "b"], [1, 2, 1, 2]]) # Rows are now ordered as the requested keys df.loc[(['b', 'a'], [2, 1]), :] + +- Bug in :meth:`MultiIndex.intersection` was not guaranteed to preserve order when ``sort=False``. (:issue:`31325`) + +.. ipython:: python + + left = pd.MultiIndex.from_arrays([["b", "a"], [2, 1]]) + right = pd.MultiIndex.from_arrays([["a", "b", "c"], [1, 2, 3]]) + # Common elements are now guaranteed to be ordered by the left side + left.intersection(right, sort=False) + - I/O @@ -185,12 +295,15 @@ I/O - Bug in :meth:`DataFrame.to_parquet` overwriting pyarrow's default for ``coerce_timestamps``; following pyarrow's default allows writing nanosecond timestamps with ``version="2.0"`` (:issue:`31652`). +- Bug in :class:`HDFStore` that caused it to set to ``int64`` the dtype of a ``datetime64`` column when reading a DataFrame in Python 3 from fixed format written in Python 2 (:issue:`31750`) Plotting ^^^^^^^^ - :func:`.plot` for line/bar now accepts color by dictonary (:issue:`8193`). - +- Bug in :meth:`DataFrame.boxplot` and :meth:`DataFrame.plot.boxplot` lost color attributes of ``medianprops``, ``whiskerprops``, ``capprops`` and ``medianprops`` (:issue:`30346`) + Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -207,8 +320,10 @@ Reshaping - Bug in :meth:`DataFrame.pivot_table` when ``margin`` is ``True`` and only ``column`` is defined (:issue:`31016`) - Fix incorrect error message in :meth:`DataFrame.pivot` when ``columns`` is set to ``None``. (:issue:`30924`) - Bug in :func:`crosstab` when inputs are two Series and have tuple names, the output will keep dummy MultiIndex as columns. (:issue:`18321`) +- :meth:`DataFrame.pivot` can now take lists for ``index`` and ``columns`` arguments (:issue:`21425`) - Bug in :func:`concat` where the resulting indices are not copied when ``copy=True`` (:issue:`29879`) + Sparse ^^^^^^ @@ -227,6 +342,8 @@ Other - Appending a dictionary to a :class:`DataFrame` without passing ``ignore_index=True`` will raise ``TypeError: Can only append a dict if ignore_index=True`` instead of ``TypeError: Can only append a Series if ignore_index=True or if the Series has a name`` (:issue:`30871`) - Set operations on an object-dtype :class:`Index` now always return object-dtype results (:issue:`31401`) +- Bug in :meth:`AbstractHolidayCalendar.holidays` when no rules were defined (:issue:`31415`) +- .. --------------------------------------------------------------------------- diff --git a/environment.yml b/environment.yml index 5f1184e921119..cbdaf8e6c4217 100644 --- a/environment.yml +++ b/environment.yml @@ -26,6 +26,7 @@ dependencies: # documentation - gitpython # obtain contributors from git for whatsnew + - gitdb2=2.0.6 # GH-32060 - sphinx # documentation (jupyter notebooks) diff --git a/flake8/cython-template.cfg b/flake8/cython-template.cfg new file mode 100644 index 0000000000000..61562bd7701b1 --- /dev/null +++ b/flake8/cython-template.cfg @@ -0,0 +1,4 @@ +[flake8] +filename = *.pxi.in +select = E501,E302,E203,E111,E114,E221,E303,E231,E126,F403 + diff --git a/flake8/cython.cfg b/flake8/cython.cfg new file mode 100644 index 0000000000000..2dfe47b60b4c1 --- /dev/null +++ b/flake8/cython.cfg @@ -0,0 +1,3 @@ +[flake8] +filename = *.pyx,*.pxd +select=E501,E302,E203,E111,E114,E221,E303,E128,E231,E126,E265,E305,E301,E127,E261,E271,E129,W291,E222,E241,E123,F403,C400,C401,C402,C403,C404,C405,C406,C407,C408,C409,C410,C411 diff --git a/pandas/__init__.py b/pandas/__init__.py index d526531b159b2..2d3d3f7d92a9c 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -25,6 +25,7 @@ _np_version_under1p16, _np_version_under1p17, _np_version_under1p18, + _is_numpy_dev, ) try: diff --git a/pandas/_config/config.py b/pandas/_config/config.py index 8b6116d3abd60..f1959cd70ed3a 100644 --- a/pandas/_config/config.py +++ b/pandas/_config/config.py @@ -82,7 +82,8 @@ class OptionError(AttributeError, KeyError): - """Exception for pandas.options, backwards compatible with KeyError + """ + Exception for pandas.options, backwards compatible with KeyError checks """ @@ -395,7 +396,6 @@ class option_context: Examples -------- - >>> with option_context('display.max_rows', 10, 'display.max_columns', 5): ... ... """ @@ -546,11 +546,11 @@ def deprecate_option( def _select_options(pat: str) -> List[str]: - """returns a list of keys matching `pat` + """ + returns a list of keys matching `pat` if pat=="all", returns all registered options """ - # short-circuit for exact key if pat in _registered_options: return [pat] @@ -573,7 +573,6 @@ def _get_root(key: str) -> Tuple[Dict[str, Any], str]: def _is_deprecated(key: str) -> bool: """ Returns True if the given option has been deprecated """ - key = key.lower() return key in _deprecated_options @@ -586,7 +585,6 @@ def _get_deprecated_option(key: str): ------- DeprecatedOption (namedtuple) if key is deprecated, None otherwise """ - try: d = _deprecated_options[key] except KeyError: @@ -611,7 +609,6 @@ def _translate_key(key: str) -> str: if key id deprecated and a replacement key defined, will return the replacement key, otherwise returns `key` as - is """ - d = _get_deprecated_option(key) if d: return d.rkey or key @@ -627,7 +624,6 @@ def _warn_if_deprecated(key: str) -> bool: ------- bool - True if `key` is deprecated, False otherwise. """ - d = _get_deprecated_option(key) if d: if d.msg: @@ -649,7 +645,6 @@ def _warn_if_deprecated(key: str) -> bool: def _build_option_description(k: str) -> str: """ Builds a formatted description of a registered option and prints it """ - o = _get_registered_option(k) d = _get_deprecated_option(k) @@ -674,7 +669,6 @@ def _build_option_description(k: str) -> str: def pp_options_list(keys: Iterable[str], width=80, _print: bool = False): """ Builds a concise listing of available options, grouped by prefix """ - from textwrap import wrap from itertools import groupby @@ -716,15 +710,16 @@ def pp(name: str, ks: Iterable[str]) -> List[str]: @contextmanager def config_prefix(prefix): - """contextmanager for multiple invocations of API with a common prefix + """ + contextmanager for multiple invocations of API with a common prefix supported API functions: (register / get / set )__option Warning: This is not thread - safe, and won't work properly if you import the API functions into your module using the "from x import y" construct. - Example: - + Example + ------- import pandas._config.config as cf with cf.config_prefix("display.font"): cf.register_option("color", "red") @@ -738,7 +733,6 @@ def config_prefix(prefix): will register options "display.font.color", "display.font.size", set the value of "display.font.size"... and so on. """ - # Note: reset_option relies on set_option, and on key directly # it does not fit in to this monkey-patching scheme @@ -801,7 +795,6 @@ def is_instance_factory(_type) -> Callable[[Any], None]: ValueError if x is not an instance of `_type` """ - if isinstance(_type, (tuple, list)): _type = tuple(_type) type_repr = "|".join(map(str, _type)) @@ -848,7 +841,6 @@ def is_nonnegative_int(value: Optional[int]) -> None: ValueError When the value is not None or is a negative integer """ - if value is None: return diff --git a/pandas/_config/localization.py b/pandas/_config/localization.py index 0d68e78372d8a..66865e1afb952 100644 --- a/pandas/_config/localization.py +++ b/pandas/_config/localization.py @@ -61,7 +61,6 @@ def can_set_locale(lc: str, lc_var: int = locale.LC_ALL) -> bool: bool Whether the passed locale can be set """ - try: with set_locale(lc, lc_var=lc_var): pass diff --git a/pandas/_libs/algos.pyx b/pandas/_libs/algos.pyx index dd1f38ce3a842..b7f17aee35a44 100644 --- a/pandas/_libs/algos.pyx +++ b/pandas/_libs/algos.pyx @@ -7,13 +7,30 @@ from libc.math cimport fabs, sqrt import numpy as np cimport numpy as cnp -from numpy cimport (ndarray, - NPY_INT64, NPY_INT32, NPY_INT16, NPY_INT8, - NPY_UINT64, NPY_UINT32, NPY_UINT16, NPY_UINT8, - NPY_FLOAT32, NPY_FLOAT64, - NPY_OBJECT, - int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, - uint32_t, uint64_t, float32_t, float64_t) +from numpy cimport ( + NPY_FLOAT32, + NPY_FLOAT64, + NPY_INT8, + NPY_INT16, + NPY_INT32, + NPY_INT64, + NPY_OBJECT, + NPY_UINT8, + NPY_UINT16, + NPY_UINT32, + NPY_UINT64, + float32_t, + float64_t, + int8_t, + int16_t, + int32_t, + int64_t, + ndarray, + uint8_t, + uint16_t, + uint32_t, + uint64_t, +) cnp.import_array() @@ -1173,12 +1190,12 @@ ctypedef fused out_t: @cython.boundscheck(False) @cython.wraparound(False) -def diff_2d(ndarray[diff_t, ndim=2] arr, - ndarray[out_t, ndim=2] out, +def diff_2d(diff_t[:, :] arr, + out_t[:, :] out, Py_ssize_t periods, int axis): cdef: Py_ssize_t i, j, sx, sy, start, stop - bint f_contig = arr.flags.f_contiguous + bint f_contig = arr.is_f_contig() # Disable for unsupported dtype combinations, # see https://github.com/cython/cython/issues/2646 diff --git a/pandas/_libs/groupby.pyx b/pandas/_libs/groupby.pyx index edc44f1c94589..27b3095d8cb4f 100644 --- a/pandas/_libs/groupby.pyx +++ b/pandas/_libs/groupby.pyx @@ -22,6 +22,8 @@ from pandas._libs.algos cimport (swap, TiebreakEnumType, TIEBREAK_AVERAGE, from pandas._libs.algos import (take_2d_axis1_float64_float64, groupsort_indexer, tiebreakers) +from pandas._libs.missing cimport checknull + cdef int64_t NPY_NAT = get_nat() _int64_max = np.iinfo(np.int64).max @@ -887,7 +889,7 @@ def group_last(rank_t[:, :] out, for j in range(K): val = values[i, j] - if val == val: + if not checknull(val): # NB: use _treat_as_na here once # conditional-nogil is available. nobs[lab, j] += 1 @@ -976,7 +978,7 @@ def group_nth(rank_t[:, :] out, for j in range(K): val = values[i, j] - if val == val: + if not checknull(val): # NB: use _treat_as_na here once # conditional-nogil is available. nobs[lab, j] += 1 diff --git a/pandas/_libs/hashing.pyx b/pandas/_libs/hashing.pyx index 878da670b2f68..2d859db22ea23 100644 --- a/pandas/_libs/hashing.pyx +++ b/pandas/_libs/hashing.pyx @@ -5,7 +5,7 @@ import cython from libc.stdlib cimport malloc, free import numpy as np -from numpy cimport uint8_t, uint32_t, uint64_t, import_array +from numpy cimport ndarray, uint8_t, uint32_t, uint64_t, import_array import_array() from pandas._libs.util cimport is_nan @@ -15,7 +15,7 @@ DEF dROUNDS = 4 @cython.boundscheck(False) -def hash_object_array(object[:] arr, object key, object encoding='utf8'): +def hash_object_array(ndarray[object] arr, object key, object encoding='utf8'): """ Parameters ---------- diff --git a/pandas/_libs/hashtable_class_helper.pxi.in b/pandas/_libs/hashtable_class_helper.pxi.in index 6671375f628e7..811025a4b5764 100644 --- a/pandas/_libs/hashtable_class_helper.pxi.in +++ b/pandas/_libs/hashtable_class_helper.pxi.in @@ -10,6 +10,7 @@ WARNING: DO NOT edit .pxi FILE directly, .pxi is generated from .pxi.in # ---------------------------------------------------------------------- from pandas._libs.tslibs.util cimport get_c_string +from pandas._libs.missing cimport C_NA {{py: @@ -1032,8 +1033,12 @@ cdef class PyObjectHashTable(HashTable): val = values[i] hash(val) - if ignore_na and ((val != val or val is None) - or (use_na_value and val == na_value)): + if ignore_na and ( + (val is C_NA) + or (val != val) + or (val is None) + or (use_na_value and val == na_value) + ): # if missing values do not count as unique values (i.e. if # ignore_na is True), skip the hashtable entry for them, and # replace the corresponding label with na_sentinel diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 4185cc2084469..6141e2b78e9f4 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -12,6 +12,7 @@ cnp.import_array() cimport pandas._libs.util as util +from pandas._libs.tslibs import Period from pandas._libs.tslibs.nattype cimport c_NaT as NaT from pandas._libs.tslibs.c_timestamp cimport _Timestamp @@ -466,6 +467,28 @@ cdef class TimedeltaEngine(DatetimeEngine): cdef class PeriodEngine(Int64Engine): + cdef int64_t _unbox_scalar(self, scalar) except? -1: + if scalar is NaT: + return scalar.value + if isinstance(scalar, Period): + # NB: we assume that we have the correct freq here. + # TODO: potential optimize by checking for _Period? + return scalar.ordinal + raise TypeError(scalar) + + cpdef get_loc(self, object val): + # NB: the caller is responsible for ensuring that we are called + # with either a Period or NaT + cdef: + int64_t conv + + try: + conv = self._unbox_scalar(val) + except TypeError: + raise KeyError(val) + + return Int64Engine.get_loc(self, conv) + cdef _get_index_values(self): return super(PeriodEngine, self).vgetter().view("i8") diff --git a/pandas/_libs/indexing.pyx b/pandas/_libs/indexing.pyx index cdccdb504571c..316943edee124 100644 --- a/pandas/_libs/indexing.pyx +++ b/pandas/_libs/indexing.pyx @@ -1,7 +1,6 @@ cdef class _NDFrameIndexerBase: """ - A base class for _NDFrameIndexer for fast instantiation and attribute - access. + A base class for _NDFrameIndexer for fast instantiation and attribute access. """ cdef public object obj, name, _ndim diff --git a/pandas/_libs/join.pyx b/pandas/_libs/join.pyx index 093c53790cd35..f696591cf3bd1 100644 --- a/pandas/_libs/join.pyx +++ b/pandas/_libs/join.pyx @@ -3,13 +3,25 @@ from cython import Py_ssize_t import numpy as np cimport numpy as cnp -from numpy cimport (ndarray, - int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, - uint32_t, uint64_t, float32_t, float64_t) +from numpy cimport ( + float32_t, + float64_t, + int8_t, + int16_t, + int32_t, + int64_t, + ndarray, + uint8_t, + uint16_t, + uint32_t, + uint64_t, +) cnp.import_array() from pandas._libs.algos import ( - groupsort_indexer, ensure_platform_int, take_1d_int64_int64 + ensure_platform_int, + groupsort_indexer, + take_1d_int64_int64, ) @@ -242,6 +254,8 @@ ctypedef fused join_t: float64_t float32_t object + int8_t + int16_t int32_t int64_t uint64_t diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index 9702eb4615909..7a18429f21a18 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -15,18 +15,33 @@ from cpython.iterator cimport PyIter_Check from cpython.sequence cimport PySequence_Check from cpython.number cimport PyNumber_Check -from cpython.datetime cimport (PyDateTime_Check, PyDate_Check, - PyTime_Check, PyDelta_Check, - PyDateTime_IMPORT) +from cpython.datetime cimport ( + PyDateTime_Check, + PyDate_Check, + PyTime_Check, + PyDelta_Check, + PyDateTime_IMPORT, +) PyDateTime_IMPORT import numpy as np cimport numpy as cnp -from numpy cimport (ndarray, PyArray_Check, PyArray_GETITEM, - PyArray_ITER_DATA, PyArray_ITER_NEXT, PyArray_IterNew, - flatiter, NPY_OBJECT, - int64_t, float32_t, float64_t, - uint8_t, uint64_t, complex128_t) +from numpy cimport ( + NPY_OBJECT, + PyArray_Check, + PyArray_GETITEM, + PyArray_ITER_DATA, + PyArray_ITER_NEXT, + PyArray_IterNew, + complex128_t, + flatiter, + float32_t, + float64_t, + int64_t, + ndarray, + uint8_t, + uint64_t, +) cnp.import_array() cdef extern from "numpy/arrayobject.h": @@ -60,7 +75,12 @@ from pandas._libs.tslibs.timedeltas cimport convert_to_timedelta64 from pandas._libs.tslibs.timezones cimport get_timezone, tz_compare from pandas._libs.missing cimport ( - checknull, isnaobj, is_null_datetime64, is_null_timedelta64, is_null_period, C_NA + checknull, + isnaobj, + is_null_datetime64, + is_null_timedelta64, + is_null_period, + C_NA, ) @@ -246,7 +266,7 @@ def item_from_zerodim(val: object) -> object: @cython.wraparound(False) @cython.boundscheck(False) -def fast_unique_multiple(list arrays, sort: bool=True): +def fast_unique_multiple(list arrays, sort: bool = True): """ Generate a list of unique values from a list of arrays. @@ -277,6 +297,7 @@ def fast_unique_multiple(list arrays, sort: bool=True): if val not in table: table[val] = stub uniques.append(val) + if sort is None: try: uniques.sort() @@ -289,7 +310,7 @@ def fast_unique_multiple(list arrays, sort: bool=True): @cython.wraparound(False) @cython.boundscheck(False) -def fast_unique_multiple_list(lists: list, sort: bool=True) -> list: +def fast_unique_multiple_list(lists: list, sort: bool = True) -> list: cdef: list buf Py_ssize_t k = len(lists) @@ -571,6 +592,8 @@ def array_equivalent_object(left: object[:], right: object[:]) -> bool: if PyArray_Check(x) and PyArray_Check(y): if not array_equivalent_object(x, y): return False + elif (x is C_NA) ^ (y is C_NA): + return False elif not (PyObject_RichCompareBool(x, y, Py_EQ) or (x is None or is_nan(x)) and (y is None or is_nan(y))): return False @@ -1005,7 +1028,7 @@ _TYPE_MAP = { 'complex64': 'complex', 'complex128': 'complex', 'c': 'complex', - 'string': 'bytes', + 'string': 'string', 'S': 'bytes', 'U': 'string', 'bool': 'boolean', diff --git a/pandas/_libs/reduction.pyx b/pandas/_libs/reduction.pyx index 43d253f632f0f..b27072aa66708 100644 --- a/pandas/_libs/reduction.pyx +++ b/pandas/_libs/reduction.pyx @@ -309,8 +309,7 @@ cdef class SeriesGrouper(_BaseGrouper): def __init__(self, object series, object f, object labels, Py_ssize_t ngroups, object dummy): - # in practice we always pass either obj[:0] or the - # safer obj._get_values(slice(None, 0)) + # in practice we always pass obj.iloc[:0] or equivalent assert dummy is not None if len(series) == 0: diff --git a/pandas/_libs/reshape.pyx b/pandas/_libs/reshape.pyx index 4e831081c8e54..e74b5919a4590 100644 --- a/pandas/_libs/reshape.pyx +++ b/pandas/_libs/reshape.pyx @@ -1,8 +1,20 @@ import cython from cython import Py_ssize_t -from numpy cimport (int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, - uint32_t, uint64_t, float32_t, float64_t, ndarray) +from numpy cimport ( + float32_t, + float64_t, + int8_t, + int16_t, + int32_t, + int64_t, + ndarray, + uint8_t, + uint16_t, + uint32_t, + uint64_t, +) + cimport numpy as cnp import numpy as np from pandas._libs.lib cimport c_is_list_like diff --git a/pandas/_libs/sparse.pyx b/pandas/_libs/sparse.pyx index 3a6dd506b2428..091ca42cb71dd 100644 --- a/pandas/_libs/sparse.pyx +++ b/pandas/_libs/sparse.pyx @@ -188,8 +188,7 @@ cdef class IntIndex(SparseIndex): return -1 @cython.wraparound(False) - cpdef ndarray[int32_t] lookup_array(self, ndarray[ - int32_t, ndim=1] indexer): + cpdef ndarray[int32_t] lookup_array(self, ndarray[int32_t, ndim=1] indexer): """ Vectorized lookup, returns ndarray[int32_t] """ @@ -424,12 +423,9 @@ cdef class BlockIndex(SparseIndex): """ Intersect two BlockIndex objects - Parameters - ---------- - Returns ------- - intersection : BlockIndex + BlockIndex """ cdef: BlockIndex y @@ -448,7 +444,7 @@ cdef class BlockIndex(SparseIndex): ylen = y.blengths # block may be split, but can't exceed original len / 2 + 1 - max_len = int(min(self.length, y.length) / 2) + 1 + max_len = min(self.length, y.length) // 2 + 1 out_bloc = np.empty(max_len, dtype=np.int32) out_blen = np.empty(max_len, dtype=np.int32) @@ -518,7 +514,7 @@ cdef class BlockIndex(SparseIndex): Returns ------- - union : BlockIndex + BlockIndex """ return BlockUnion(self, y.to_block_index()).result @@ -548,8 +544,7 @@ cdef class BlockIndex(SparseIndex): return -1 @cython.wraparound(False) - cpdef ndarray[int32_t] lookup_array(self, ndarray[ - int32_t, ndim=1] indexer): + cpdef ndarray[int32_t] lookup_array(self, ndarray[int32_t, ndim=1] indexer): """ Vectorized lookup, returns ndarray[int32_t] """ @@ -672,7 +667,7 @@ cdef class BlockUnion(BlockMerge): ystart = self.ystart yend = self.yend - max_len = int(min(self.x.length, self.y.length) / 2) + 1 + max_len = min(self.x.length, self.y.length) // 2 + 1 out_bloc = np.empty(max_len, dtype=np.int32) out_blen = np.empty(max_len, dtype=np.int32) diff --git a/pandas/_libs/sparse_op_helper.pxi.in b/pandas/_libs/sparse_op_helper.pxi.in index 996da4ca2f92b..ce665ca812131 100644 --- a/pandas/_libs/sparse_op_helper.pxi.in +++ b/pandas/_libs/sparse_op_helper.pxi.in @@ -235,7 +235,7 @@ cdef inline tuple int_op_{{opname}}_{{dtype}}({{dtype}}_t[:] x_, {{dtype}}_t yfill): cdef: IntIndex out_index - Py_ssize_t xi = 0, yi = 0, out_i = 0 # fp buf indices + Py_ssize_t xi = 0, yi = 0, out_i = 0 # fp buf indices int32_t xloc, yloc int32_t[:] xindices, yindices, out_indices {{dtype}}_t[:] x, y diff --git a/pandas/_libs/src/inline_helper.h b/pandas/_libs/src/inline_helper.h index e203a05d2eb56..40fd45762ffe4 100644 --- a/pandas/_libs/src/inline_helper.h +++ b/pandas/_libs/src/inline_helper.h @@ -11,7 +11,9 @@ The full license is in the LICENSE file, distributed with this software. #define PANDAS__LIBS_SRC_INLINE_HELPER_H_ #ifndef PANDAS_INLINE - #if defined(__GNUC__) + #if defined(__clang__) + #define PANDAS_INLINE static __inline__ __attribute__ ((__unused__)) + #elif defined(__GNUC__) #define PANDAS_INLINE static __inline__ #elif defined(_MSC_VER) #define PANDAS_INLINE static __inline diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 53e3354ca8eb6..a176c4e41e834 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -49,30 +49,31 @@ from pandas._libs.tslibs.tzconversion cimport ( cdef inline object create_datetime_from_ts( int64_t value, npy_datetimestruct dts, - object tz, object freq): + object tz, object freq, bint fold): """ convenience routine to construct a datetime.datetime from its parts """ return datetime(dts.year, dts.month, dts.day, dts.hour, - dts.min, dts.sec, dts.us, tz) + dts.min, dts.sec, dts.us, tz, fold=fold) cdef inline object create_date_from_ts( int64_t value, npy_datetimestruct dts, - object tz, object freq): + object tz, object freq, bint fold): """ convenience routine to construct a datetime.date from its parts """ + # GH 25057 add fold argument to match other func_create signatures return date(dts.year, dts.month, dts.day) cdef inline object create_time_from_ts( int64_t value, npy_datetimestruct dts, - object tz, object freq): + object tz, object freq, bint fold): """ convenience routine to construct a datetime.time from its parts """ - return time(dts.hour, dts.min, dts.sec, dts.us, tz) + return time(dts.hour, dts.min, dts.sec, dts.us, tz, fold=fold) @cython.wraparound(False) @cython.boundscheck(False) def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, - str box="datetime"): + bint fold=0, str box="datetime"): """ Convert an i8 repr to an ndarray of datetimes, date, time or Timestamp @@ -83,6 +84,13 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, convert to this timezone freq : str/Offset, default None freq to convert + fold : bint, default is 0 + Due to daylight saving time, one wall clock time can occur twice + when shifting from summer to winter time; fold describes whether the + datetime-like corresponds to the first (0) or the second time (1) + the wall clock hits the ambiguous time + + .. versionadded:: 1.1.0 box : {'datetime', 'timestamp', 'date', 'time'}, default 'datetime' If datetime, convert to datetime.datetime If date, convert to datetime.date @@ -104,7 +112,7 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, str typ int64_t value, delta, local_value ndarray[object] result = np.empty(n, dtype=object) - object (*func_create)(int64_t, npy_datetimestruct, object, object) + object (*func_create)(int64_t, npy_datetimestruct, object, object, bint) if box == "date": assert (tz is None), "tz should be None when converting to date" @@ -129,7 +137,7 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, result[i] = NaT else: dt64_to_dtstruct(value, &dts) - result[i] = func_create(value, dts, tz, freq) + result[i] = func_create(value, dts, tz, freq, fold) elif is_tzlocal(tz): for i in range(n): value = arr[i] @@ -141,7 +149,7 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, # using the i8 representation. local_value = tz_convert_utc_to_tzlocal(value, tz) dt64_to_dtstruct(local_value, &dts) - result[i] = func_create(value, dts, tz, freq) + result[i] = func_create(value, dts, tz, freq, fold) else: trans, deltas, typ = get_dst_info(tz) @@ -155,7 +163,7 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, else: # Adjust datetime64 timestamp, recompute datetimestruct dt64_to_dtstruct(value + delta, &dts) - result[i] = func_create(value, dts, tz, freq) + result[i] = func_create(value, dts, tz, freq, fold) elif typ == 'dateutil': # no zone-name change for dateutil tzs - dst etc @@ -168,7 +176,7 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, # Adjust datetime64 timestamp, recompute datetimestruct pos = trans.searchsorted(value, side='right') - 1 dt64_to_dtstruct(value + deltas[pos], &dts) - result[i] = func_create(value, dts, tz, freq) + result[i] = func_create(value, dts, tz, freq, fold) else: # pytz for i in range(n): @@ -182,7 +190,7 @@ def ints_to_pydatetime(const int64_t[:] arr, object tz=None, object freq=None, new_tz = tz._tzinfos[tz._transition_info[pos]] dt64_to_dtstruct(value + deltas[pos], &dts) - result[i] = func_create(value, dts, new_tz, freq) + result[i] = func_create(value, dts, new_tz, freq, fold) return result diff --git a/pandas/_libs/tslibs/conversion.pxd b/pandas/_libs/tslibs/conversion.pxd index c74307a3d2887..bb20296e24587 100644 --- a/pandas/_libs/tslibs/conversion.pxd +++ b/pandas/_libs/tslibs/conversion.pxd @@ -12,6 +12,7 @@ cdef class _TSObject: npy_datetimestruct dts # npy_datetimestruct int64_t value # numpy dt64 object tzinfo + bint fold cdef convert_to_tsobject(object ts, object tz, object unit, diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index bf38fcfb6103c..57483783faf9f 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -39,7 +39,8 @@ from pandas._libs.tslibs.nattype cimport ( from pandas._libs.tslibs.tzconversion import ( tz_localize_to_utc, tz_convert_single) -from pandas._libs.tslibs.tzconversion cimport _tz_convert_tzlocal_utc +from pandas._libs.tslibs.tzconversion cimport ( + _tz_convert_tzlocal_utc, _tz_convert_tzlocal_fromutc) # ---------------------------------------------------------------------- # Constants @@ -84,12 +85,11 @@ def ensure_datetime64ns(arr: ndarray, copy: bool=True): Parameters ---------- arr : ndarray - copy : boolean, default True + copy : bool, default True Returns ------- - result : ndarray with dtype datetime64[ns] - + ndarray with dtype datetime64[ns] """ cdef: Py_ssize_t i, n = arr.size @@ -152,7 +152,7 @@ def ensure_timedelta64ns(arr: ndarray, copy: bool=True): @cython.boundscheck(False) @cython.wraparound(False) -def datetime_to_datetime64(object[:] values): +def datetime_to_datetime64(ndarray[object] values): """ Convert ndarray of datetime-like objects to int64 array representing nanosecond timestamps. @@ -216,6 +216,11 @@ cdef class _TSObject: # npy_datetimestruct dts # npy_datetimestruct # int64_t value # numpy dt64 # object tzinfo + # bint fold + + def __cinit__(self): + # GH 25057. As per PEP 495, set fold to 0 by default + self.fold = 0 @property def value(self): @@ -323,6 +328,7 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz, cdef: _TSObject obj = _TSObject() + obj.fold = ts.fold if tz is not None: tz = maybe_get_tz(tz) @@ -381,6 +387,8 @@ cdef _TSObject create_tsobject_tz_using_offset(npy_datetimestruct dts, _TSObject obj = _TSObject() int64_t value # numpy dt64 datetime dt + ndarray[int64_t] trans + int64_t[:] deltas value = dtstruct_to_dt64(&dts) obj.dts = dts @@ -390,10 +398,23 @@ cdef _TSObject create_tsobject_tz_using_offset(npy_datetimestruct dts, check_overflows(obj) return obj + # Infer fold from offset-adjusted obj.value + # see PEP 495 https://www.python.org/dev/peps/pep-0495/#the-fold-attribute + if is_utc(tz): + pass + elif is_tzlocal(tz): + _tz_convert_tzlocal_fromutc(obj.value, tz, &obj.fold) + else: + trans, deltas, typ = get_dst_info(tz) + + if typ == 'dateutil': + pos = trans.searchsorted(obj.value, side='right') - 1 + obj.fold = _infer_tsobject_fold(obj, trans, deltas, pos) + # Keep the converter same as PyDateTime's dt = datetime(obj.dts.year, obj.dts.month, obj.dts.day, obj.dts.hour, obj.dts.min, obj.dts.sec, - obj.dts.us, obj.tzinfo) + obj.dts.us, obj.tzinfo, fold=obj.fold) obj = convert_datetime_to_tsobject( dt, tz, nanos=obj.dts.ps // 1000) return obj @@ -544,7 +565,7 @@ cdef inline void localize_tso(_TSObject obj, tzinfo tz): elif obj.value == NPY_NAT: pass elif is_tzlocal(tz): - local_val = _tz_convert_tzlocal_utc(obj.value, tz, to_utc=False) + local_val = _tz_convert_tzlocal_fromutc(obj.value, tz, &obj.fold) dt64_to_dtstruct(local_val, &obj.dts) else: # Adjust datetime64 timestamp, recompute datetimestruct @@ -563,6 +584,8 @@ cdef inline void localize_tso(_TSObject obj, tzinfo tz): # i.e. treat_tz_as_dateutil(tz) pos = trans.searchsorted(obj.value, side='right') - 1 dt64_to_dtstruct(obj.value + deltas[pos], &obj.dts) + # dateutil supports fold, so we infer fold from value + obj.fold = _infer_tsobject_fold(obj, trans, deltas, pos) else: # Note: as of 2018-07-17 all tzinfo objects that are _not_ # either pytz or dateutil have is_fixed_offset(tz) == True, @@ -572,6 +595,45 @@ cdef inline void localize_tso(_TSObject obj, tzinfo tz): obj.tzinfo = tz +cdef inline bint _infer_tsobject_fold(_TSObject obj, ndarray[int64_t] trans, + int64_t[:] deltas, int32_t pos): + """ + Infer _TSObject fold property from value by assuming 0 and then setting + to 1 if necessary. + + Parameters + ---------- + obj : _TSObject + trans : ndarray[int64_t] + ndarray of offset transition points in nanoseconds since epoch. + deltas : int64_t[:] + array of offsets corresponding to transition points in trans. + pos : int32_t + Position of the last transition point before taking fold into account. + + Returns + ------- + bint + Due to daylight saving time, one wall clock time can occur twice + when shifting from summer to winter time; fold describes whether the + datetime-like corresponds to the first (0) or the second time (1) + the wall clock hits the ambiguous time + + References + ---------- + .. [1] "PEP 495 - Local Time Disambiguation" + https://www.python.org/dev/peps/pep-0495/#the-fold-attribute + """ + cdef: + bint fold = 0 + + if pos > 0: + fold_delta = deltas[pos - 1] - deltas[pos] + if obj.value - fold_delta < trans[pos]: + fold = 1 + + return fold + cdef inline datetime _localize_pydatetime(datetime dt, tzinfo tz): """ Take a datetime/Timestamp in UTC and localizes to timezone tz. diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 9f6f401a1a5f5..68a25d0cc481a 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -22,6 +22,8 @@ from pandas._libs.tslibs.util cimport ( get_nat, is_integer_object, is_float_object, is_datetime64_object, is_timedelta64_object) +from pandas._libs.missing cimport C_NA + # ---------------------------------------------------------------------- # Constants @@ -763,7 +765,7 @@ NaT = c_NaT # Python-visible cdef inline bint checknull_with_nat(object val): """ utility to check if a value is a nat or not """ - return val is None or util.is_nan(val) or val is c_NaT + return val is None or util.is_nan(val) or val is c_NaT or val is C_NA cpdef bint is_null_datetimelike(object val, bint inat_is_null=True): diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 9419f0eba39aa..c3a47902cff0f 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1,8 +1,6 @@ from datetime import datetime -from cpython.object cimport ( - PyObject_RichCompareBool, - Py_EQ, Py_NE) +from cpython.object cimport PyObject_RichCompareBool, Py_EQ, Py_NE from numpy cimport int64_t, import_array, ndarray import numpy as np @@ -14,15 +12,25 @@ from libc.string cimport strlen, memset import cython -from cpython.datetime cimport (PyDateTime_Check, PyDelta_Check, PyDate_Check, - PyDateTime_IMPORT) +from cpython.datetime cimport ( + PyDate_Check, + PyDateTime_Check, + PyDateTime_IMPORT, + PyDelta_Check, +) # import datetime C API PyDateTime_IMPORT from pandas._libs.tslibs.np_datetime cimport ( - npy_datetimestruct, dtstruct_to_dt64, dt64_to_dtstruct, - pandas_datetime_to_datetimestruct, check_dts_bounds, - NPY_DATETIMEUNIT, NPY_FR_D, NPY_FR_us) + npy_datetimestruct, + dtstruct_to_dt64, + dt64_to_dtstruct, + pandas_datetime_to_datetimestruct, + check_dts_bounds, + NPY_DATETIMEUNIT, + NPY_FR_D, + NPY_FR_us, +) cdef extern from "src/datetime/np_datetime.h": int64_t npy_datetimestruct_to_datetime(NPY_DATETIMEUNIT fr, @@ -37,12 +45,15 @@ from pandas._libs.tslibs.timedeltas import Timedelta from pandas._libs.tslibs.timedeltas cimport delta_to_nanoseconds cimport pandas._libs.tslibs.ccalendar as ccalendar -from pandas._libs.tslibs.ccalendar cimport ( - dayofweek, get_day_of_year, is_leapyear) +from pandas._libs.tslibs.ccalendar cimport dayofweek, get_day_of_year, is_leapyear from pandas._libs.tslibs.ccalendar import MONTH_NUMBERS from pandas._libs.tslibs.frequencies cimport ( - get_freq_code, get_base_alias, get_to_timestamp_base, get_freq_str, - get_rule_month) + get_base_alias, + get_freq_code, + get_freq_str, + get_rule_month, + get_to_timestamp_base, +) from pandas._libs.tslibs.parsing import parse_time_string from pandas._libs.tslibs.resolution import Resolution from pandas._libs.tslibs.nattype import nat_strings @@ -55,7 +66,7 @@ from pandas._libs.tslibs.tzconversion cimport tz_convert_utc_to_tzlocal cdef: enum: - INT32_MIN = -2147483648 + INT32_MIN = -2_147_483_648 ctypedef struct asfreq_info: @@ -179,8 +190,7 @@ cdef freq_conv_func get_asfreq_func(int from_freq, int to_freq) nogil: return asfreq_MtoB elif from_group == FR_WK: return asfreq_WtoB - elif from_group in [FR_DAY, FR_HR, FR_MIN, FR_SEC, - FR_MS, FR_US, FR_NS]: + elif from_group in [FR_DAY, FR_HR, FR_MIN, FR_SEC, FR_MS, FR_US, FR_NS]: return asfreq_DTtoB else: return nofunc @@ -289,17 +299,15 @@ cdef int64_t DtoB(npy_datetimestruct *dts, int roll_back, return DtoB_weekday(unix_date) -cdef inline int64_t upsample_daytime(int64_t ordinal, - asfreq_info *af_info) nogil: - if (af_info.is_end): +cdef inline int64_t upsample_daytime(int64_t ordinal, asfreq_info *af_info) nogil: + if af_info.is_end: return (ordinal + 1) * af_info.intraday_conversion_factor - 1 else: return ordinal * af_info.intraday_conversion_factor -cdef inline int64_t downsample_daytime(int64_t ordinal, - asfreq_info *af_info) nogil: - return ordinal // (af_info.intraday_conversion_factor) +cdef inline int64_t downsample_daytime(int64_t ordinal, asfreq_info *af_info) nogil: + return ordinal // af_info.intraday_conversion_factor cdef inline int64_t transform_via_day(int64_t ordinal, @@ -1464,24 +1472,24 @@ def extract_freq(ndarray[object] values): cdef: Py_ssize_t i, n = len(values) - object p + object value for i in range(n): - p = values[i] + value = values[i] try: # now Timestamp / NaT has freq attr - if is_period_object(p): - return p.freq + if is_period_object(value): + return value.freq except AttributeError: pass raise ValueError('freq not specified and cannot be inferred') - # ----------------------------------------------------------------------- # period helpers + @cython.wraparound(False) @cython.boundscheck(False) cdef int64_t[:] localize_dt64arr_to_period(const int64_t[:] stamps, diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index 1e0eb7f97ec54..ecf31c15bb72c 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -110,8 +110,8 @@ def get_freq_group(freq) -> int: """ Return frequency code group of given frequency str or offset. - Example - ------- + Examples + -------- >>> get_freq_group('W-MON') 4000 @@ -193,8 +193,8 @@ class Resolution: """ Return resolution str against resolution code. - Example - ------- + Examples + -------- >>> Resolution.get_str(Resolution.RESO_SEC) 'second' """ @@ -205,8 +205,8 @@ class Resolution: """ Return resolution str against resolution code. - Example - ------- + Examples + -------- >>> Resolution.get_reso('second') 2 @@ -220,8 +220,8 @@ class Resolution: """ Return frequency str against resolution str. - Example - ------- + Examples + -------- >>> f.Resolution.get_freq_group('day') 4000 """ @@ -232,8 +232,8 @@ class Resolution: """ Return frequency str against resolution str. - Example - ------- + Examples + -------- >>> f.Resolution.get_freq('day') 'D' """ @@ -244,8 +244,8 @@ class Resolution: """ Return resolution str against frequency str. - Example - ------- + Examples + -------- >>> Resolution.get_str_from_freq('H') 'hour' """ @@ -256,8 +256,8 @@ class Resolution: """ Return resolution code against frequency str. - Example - ------- + Examples + -------- >>> Resolution.get_reso_from_freq('H') 4 @@ -273,8 +273,8 @@ class Resolution: Parameters ---------- - value : integer or float - freq : string + value : int or float + freq : str Frequency string Raises @@ -282,8 +282,8 @@ class Resolution: ValueError If the float cannot be converted to an integer at any resolution. - Example - ------- + Examples + -------- >>> Resolution.get_stride_from_decimal(1.5, 'T') (90, 'S') @@ -298,8 +298,9 @@ class Resolution: else: start_reso = cls.get_reso_from_freq(freq) if start_reso == 0: - raise ValueError("Could not convert to integer offset " - "at any resolution") + raise ValueError( + "Could not convert to integer offset at any resolution" + ) next_value = cls._reso_mult_map[start_reso] * value next_name = cls._reso_str_bump_map[freq] diff --git a/pandas/_libs/tslibs/strptime.pyx b/pandas/_libs/tslibs/strptime.pyx index 5508b208de00a..dfe050c7bbff7 100644 --- a/pandas/_libs/tslibs/strptime.pyx +++ b/pandas/_libs/tslibs/strptime.pyx @@ -45,8 +45,7 @@ cdef dict _parse_code_table = {'y': 0, 'u': 22} -def array_strptime(object[:] values, object fmt, - bint exact=True, errors='raise'): +def array_strptime(object[:] values, object fmt, bint exact=True, errors='raise'): """ Calculates the datetime structs represented by the passed array of strings @@ -78,12 +77,9 @@ def array_strptime(object[:] values, object fmt, if fmt is not None: if '%W' in fmt or '%U' in fmt: if '%Y' not in fmt and '%y' not in fmt: - raise ValueError("Cannot use '%W' or '%U' without " - "day and year") - if ('%A' not in fmt and '%a' not in fmt and '%w' not - in fmt): - raise ValueError("Cannot use '%W' or '%U' without " - "day and year") + raise ValueError("Cannot use '%W' or '%U' without day and year") + if '%A' not in fmt and '%a' not in fmt and '%w' not in fmt: + raise ValueError("Cannot use '%W' or '%U' without day and year") elif '%Z' in fmt and '%z' in fmt: raise ValueError("Cannot parse both %Z and %z") @@ -749,6 +745,6 @@ cdef parse_timezone_directive(str z): microseconds = int(gmtoff_remainder + gmtoff_remainder_padding) total_minutes = ((hours * 60) + minutes + (seconds // 60) + - (microseconds // 60000000)) + (microseconds // 60_000_000)) total_minutes = -total_minutes if z.startswith("-") else total_minutes return pytz.FixedOffset(total_minutes) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 3742506a7f8af..66660c5f641fd 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -37,51 +37,61 @@ from pandas._libs.tslibs.offsets import _Tick as Tick # Constants # components named tuple -Components = collections.namedtuple('Components', [ - 'days', 'hours', 'minutes', 'seconds', - 'milliseconds', 'microseconds', 'nanoseconds']) - - -cdef dict timedelta_abbrevs = { 'Y': 'Y', - 'y': 'Y', - 'M': 'M', - 'W': 'W', - 'w': 'W', - 'D': 'D', - 'd': 'D', - 'days': 'D', - 'day': 'D', - 'hours': 'h', - 'hour': 'h', - 'hr': 'h', - 'h': 'h', - 'm': 'm', - 'minute': 'm', - 'min': 'm', - 'minutes': 'm', - 't': 'm', - 's': 's', - 'seconds': 's', - 'sec': 's', - 'second': 's', - 'ms': 'ms', - 'milliseconds': 'ms', - 'millisecond': 'ms', - 'milli': 'ms', - 'millis': 'ms', - 'l': 'ms', - 'us': 'us', - 'microseconds': 'us', - 'microsecond': 'us', - 'micro': 'us', - 'micros': 'us', - 'u': 'us', - 'ns': 'ns', - 'nanoseconds': 'ns', - 'nano': 'ns', - 'nanos': 'ns', - 'nanosecond': 'ns', - 'n': 'ns'} +Components = collections.namedtuple( + "Components", + [ + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + ], +) + +cdef dict timedelta_abbrevs = { + "Y": "Y", + "y": "Y", + "M": "M", + "W": "W", + "w": "W", + "D": "D", + "d": "D", + "days": "D", + "day": "D", + "hours": "h", + "hour": "h", + "hr": "h", + "h": "h", + "m": "m", + "minute": "m", + "min": "m", + "minutes": "m", + "t": "m", + "s": "s", + "seconds": "s", + "sec": "s", + "second": "s", + "ms": "ms", + "milliseconds": "ms", + "millisecond": "ms", + "milli": "ms", + "millis": "ms", + "l": "ms", + "us": "us", + "microseconds": "us", + "microsecond": "us", + "micro": "us", + "micros": "us", + "u": "us", + "ns": "ns", + "nanoseconds": "ns", + "nano": "ns", + "nanos": "ns", + "nanosecond": "ns", + "n": "ns", +} _no_input = object() @@ -137,9 +147,11 @@ cpdef int64_t delta_to_nanoseconds(delta) except? -1: if is_integer_object(delta): return delta if PyDelta_Check(delta): - return (delta.days * 24 * 60 * 60 * 1000000 + - delta.seconds * 1000000 + - delta.microseconds) * 1000 + return ( + delta.days * 24 * 60 * 60 * 1_000_000 + + delta.seconds * 1_000_000 + + delta.microseconds + ) * 1000 raise TypeError(type(delta)) @@ -212,9 +224,8 @@ def array_to_timedelta64(object[:] values, unit='ns', errors='raise'): Py_ssize_t i, n int64_t[:] iresult - if errors not in ('ignore', 'raise', 'coerce'): - raise ValueError("errors must be one of 'ignore', " - "'raise', or 'coerce'}") + if errors not in {'ignore', 'raise', 'coerce'}: + raise ValueError("errors must be one of {'ignore', 'raise', or 'coerce'}") n = values.shape[0] result = np.empty(n, dtype='m8[ns]') @@ -255,34 +266,34 @@ cpdef inline object precision_from_unit(object unit): int p if unit == 'Y': - m = 1000000000L * 31556952 + m = 1000000000 * 31556952 p = 9 elif unit == 'M': - m = 1000000000L * 2629746 + m = 1000000000 * 2629746 p = 9 elif unit == 'W': - m = 1000000000L * DAY_SECONDS * 7 + m = 1000000000 * DAY_SECONDS * 7 p = 9 elif unit == 'D' or unit == 'd': - m = 1000000000L * DAY_SECONDS + m = 1000000000 * DAY_SECONDS p = 9 elif unit == 'h': - m = 1000000000L * 3600 + m = 1000000000 * 3600 p = 9 elif unit == 'm': - m = 1000000000L * 60 + m = 1000000000 * 60 p = 9 elif unit == 's': - m = 1000000000L + m = 1000000000 p = 9 elif unit == 'ms': - m = 1000000L + m = 1000000 p = 6 elif unit == 'us': - m = 1000L + m = 1000 p = 3 elif unit == 'ns' or unit is None: - m = 1L + m = 1 p = 0 else: raise ValueError(f"cannot cast unit {unit}") @@ -383,13 +394,13 @@ cdef inline int64_t parse_timedelta_string(str ts) except? -1: if len(number): if current_unit is None: current_unit = 'h' - m = 1000000000L * 3600 + m = 1000000000 * 3600 elif current_unit == 'h': current_unit = 'm' - m = 1000000000L * 60 + m = 1000000000 * 60 elif current_unit == 'm': current_unit = 's' - m = 1000000000L + m = 1000000000 r = int(''.join(number)) * m result += timedelta_as_neg(r, neg) have_hhmmss = 1 @@ -408,7 +419,7 @@ cdef inline int64_t parse_timedelta_string(str ts) except? -1: # hh:mm:ss (so current_unit is 'm') if current_unit != 'm': raise ValueError("expected hh:mm:ss format before .") - m = 1000000000L + m = 1000000000 r = int(''.join(number)) * m result += timedelta_as_neg(r, neg) have_value = 1 @@ -437,9 +448,9 @@ cdef inline int64_t parse_timedelta_string(str ts) except? -1: raise ValueError("no units specified") if len(frac) > 0 and len(frac) <= 3: - m = 10**(3 -len(frac)) * 1000L * 1000L + m = 10**(3 -len(frac)) * 1000 * 1000 elif len(frac) > 3 and len(frac) <= 6: - m = 10**(6 -len(frac)) * 1000L + m = 10**(6 -len(frac)) * 1000 else: m = 10**(9 -len(frac)) @@ -451,7 +462,7 @@ cdef inline int64_t parse_timedelta_string(str ts) except? -1: elif current_unit is not None: if current_unit != 'm': raise ValueError("expected hh:mm:ss format") - m = 1000000000L + m = 1000000000 r = int(''.join(number)) * m result += timedelta_as_neg(r, neg) @@ -1018,6 +1029,7 @@ cdef class _Timedelta(timedelta): **Using string input** >>> td = pd.Timedelta('1 days 2 min 3 us 42 ns') + >>> td.nanoseconds 42 @@ -1095,7 +1107,7 @@ cdef class _Timedelta(timedelta): Returns ------- - formatted : str + str See Also -------- @@ -1115,6 +1127,7 @@ cdef class _Timedelta(timedelta): -------- >>> td = pd.Timedelta(days=6, minutes=50, seconds=3, ... milliseconds=10, microseconds=10, nanoseconds=12) + >>> td.isoformat() 'P6DT0H50M3.010010012S' >>> pd.Timedelta(hours=1, seconds=10).isoformat() @@ -1190,10 +1203,12 @@ class Timedelta(_Timedelta): value = nano + convert_to_timedelta64(timedelta(**kwargs), 'ns') except TypeError as e: - raise ValueError("cannot construct a Timedelta from the " - "passed arguments, allowed keywords are " - "[weeks, days, hours, minutes, seconds, " - "milliseconds, microseconds, nanoseconds]") + raise ValueError( + "cannot construct a Timedelta from the passed arguments, " + "allowed keywords are " + "[weeks, days, hours, minutes, seconds, " + "milliseconds, microseconds, nanoseconds]" + ) if unit in {'Y', 'y', 'M'}: raise ValueError( @@ -1230,8 +1245,9 @@ class Timedelta(_Timedelta): return NaT else: raise ValueError( - f"Value must be Timedelta, string, integer, " - f"float, timedelta or convertible, not {type(value).__name__}") + "Value must be Timedelta, string, integer, " + f"float, timedelta or convertible, not {type(value).__name__}" + ) if is_timedelta64_object(value): value = value.view('i8') @@ -1509,10 +1525,13 @@ cdef _rfloordiv(int64_t value, right): return right // value -cdef _broadcast_floordiv_td64(int64_t value, object other, - object (*operation)(int64_t value, - object right)): - """Boilerplate code shared by Timedelta.__floordiv__ and +cdef _broadcast_floordiv_td64( + int64_t value, + object other, + object (*operation)(int64_t value, object right) +): + """ + Boilerplate code shared by Timedelta.__floordiv__ and Timedelta.__rfloordiv__ because np.timedelta64 does not implement these. Parameters diff --git a/pandas/_libs/tslibs/timestamps.pxd b/pandas/_libs/tslibs/timestamps.pxd index b7282e02ff117..5e55e6e8d5297 100644 --- a/pandas/_libs/tslibs/timestamps.pxd +++ b/pandas/_libs/tslibs/timestamps.pxd @@ -5,4 +5,4 @@ from pandas._libs.tslibs.np_datetime cimport npy_datetimestruct cdef object create_timestamp_from_ts(int64_t value, npy_datetimestruct dts, - object tz, object freq) + object tz, object freq, bint fold) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index b8c462abe35f1..5cd3467eed042 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -6,12 +6,12 @@ from numpy cimport int64_t cnp.import_array() from datetime import time as datetime_time, timedelta -from cpython.datetime cimport (datetime, +from cpython.datetime cimport (datetime, PyDateTime_Check, PyTZInfo_Check, PyDateTime_IMPORT) PyDateTime_IMPORT from pandas._libs.tslibs.util cimport ( - is_integer_object, is_offset_object) + is_datetime64_object, is_float_object, is_integer_object, is_offset_object) from pandas._libs.tslibs.c_timestamp cimport _Timestamp cimport pandas._libs.tslibs.ccalendar as ccalendar @@ -41,12 +41,12 @@ _no_input = object() cdef inline object create_timestamp_from_ts(int64_t value, npy_datetimestruct dts, - object tz, object freq): + object tz, object freq, bint fold): """ convenience routine to construct a Timestamp from its parts """ cdef _Timestamp ts_base ts_base = _Timestamp.__new__(Timestamp, dts.year, dts.month, dts.day, dts.hour, dts.min, - dts.sec, dts.us, tz) + dts.sec, dts.us, tz, fold=fold) ts_base.value = value ts_base.freq = freq ts_base.nanosecond = dts.ps // 1000 @@ -195,6 +195,13 @@ class Timestamp(_Timestamp): nanosecond : int, optional, default 0 .. versionadded:: 0.23.0 tzinfo : datetime.tzinfo, optional, default None + fold : {0, 1}, default None, keyword-only + Due to daylight saving time, one wall clock time can occur twice + when shifting from summer to winter time; fold describes whether the + datetime-like corresponds to the first (0) or the second time (1) + the wall clock hits the ambiguous time + + .. versionadded:: 1.1.0 Notes ----- @@ -350,7 +357,9 @@ class Timestamp(_Timestamp): second=None, microsecond=None, nanosecond=None, - tzinfo=None + tzinfo=None, + *, + fold=None ): # The parameter list folds together legacy parameter names (the first # four) and positional and keyword parameter names from pydatetime. @@ -390,6 +399,32 @@ class Timestamp(_Timestamp): # User passed tzinfo instead of tz; avoid silently ignoring tz, tzinfo = tzinfo, None + # Allow fold only for unambiguous input + if fold is not None: + if fold not in [0, 1]: + raise ValueError( + "Valid values for the fold argument are None, 0, or 1." + ) + + if (ts_input is not _no_input and not ( + PyDateTime_Check(ts_input) and + getattr(ts_input, 'tzinfo', None) is None)): + raise ValueError( + "Cannot pass fold with possibly unambiguous input: int, " + "float, numpy.datetime64, str, or timezone-aware " + "datetime-like. Pass naive datetime-like or build " + "Timestamp from components." + ) + + if tz is not None and treat_tz_as_pytz(tz): + raise ValueError( + "pytz timezones do not support fold. Please use dateutil " + "timezones." + ) + + if hasattr(ts_input, 'fold'): + ts_input = ts_input.replace(fold=fold) + # GH 30543 if pd.Timestamp already passed, return it # check that only ts_input is passed # checking verbosely, because cython doesn't optimize @@ -411,16 +446,32 @@ class Timestamp(_Timestamp): ) elif ts_input is _no_input: - # User passed keyword arguments. - ts_input = datetime(year, month, day, hour or 0, - minute or 0, second or 0, - microsecond or 0) + # GH 31200 + # When year, month or day is not given, we call the datetime + # constructor to make sure we get the same error message + # since Timestamp inherits datetime + datetime_kwargs = { + "hour": hour or 0, + "minute": minute or 0, + "second": second or 0, + "microsecond": microsecond or 0, + "fold": fold or 0 + } + if year is not None: + datetime_kwargs["year"] = year + if month is not None: + datetime_kwargs["month"] = month + if day is not None: + datetime_kwargs["day"] = day + + ts_input = datetime(**datetime_kwargs) + elif is_integer_object(freq): # User passed positional arguments: # Timestamp(year, month, day[, hour[, minute[, second[, # microsecond[, nanosecond[, tzinfo]]]]]]) ts_input = datetime(ts_input, freq, tz, unit or 0, - year or 0, month or 0, day or 0) + year or 0, month or 0, day or 0, fold=fold or 0) nanosecond = hour tz = minute freq = None @@ -440,7 +491,7 @@ class Timestamp(_Timestamp): elif not is_offset_object(freq): freq = to_offset(freq) - return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq) + return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq, ts.fold) def _round(self, freq, mode, ambiguous='raise', nonexistent='raise'): if self.tz is not None: @@ -984,7 +1035,7 @@ default 'raise' if value != NPY_NAT: check_dts_bounds(&dts) - return create_timestamp_from_ts(value, dts, _tzinfo, self.freq) + return create_timestamp_from_ts(value, dts, _tzinfo, self.freq, fold) def isoformat(self, sep='T'): base = super(_Timestamp, self).isoformat(sep=sep) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 35ee87e714fa8..0ec3e2ad467e1 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -2,9 +2,11 @@ from datetime import timezone # dateutil compat from dateutil.tz import ( - tzutc as _dateutil_tzutc, + tzfile as _dateutil_tzfile, tzlocal as _dateutil_tzlocal, - tzfile as _dateutil_tzfile) + tzutc as _dateutil_tzutc, +) + from dateutil.tz import gettz as dateutil_gettz @@ -103,7 +105,9 @@ cpdef inline object maybe_get_tz(object tz): def _p_tz_cache_key(tz): - """ Python interface for cache function to facilitate testing.""" + """ + Python interface for cache function to facilitate testing. + """ return tz_cache_key(tz) @@ -120,7 +124,7 @@ cdef inline object tz_cache_key(object tz): dateutil timezones. Notes - ===== + ----- This cannot just be the hash of a timezone object. Unfortunately, the hashes of two dateutil tz objects which represent the same timezone are not equal (even though the tz objects will compare equal and represent @@ -196,7 +200,7 @@ cdef int64_t[:] unbox_utcoffsets(object transinfo): arr = np.empty(sz, dtype='i8') for i in range(sz): - arr[i] = int(transinfo[i][0].total_seconds()) * 1000000000 + arr[i] = int(transinfo[i][0].total_seconds()) * 1_000_000_000 return arr @@ -217,7 +221,7 @@ cdef object get_dst_info(object tz): if cache_key is None: # e.g. pytz.FixedOffset, matplotlib.dates._UTC, # psycopg2.tz.FixedOffsetTimezone - num = int(get_utcoffset(tz, None).total_seconds()) * 1000000000 + num = int(get_utcoffset(tz, None).total_seconds()) * 1_000_000_000 return (np.array([NPY_NAT + 1], dtype=np.int64), np.array([num], dtype=np.int64), None) @@ -313,7 +317,7 @@ cpdef bint tz_compare(object start, object end): Returns: ------- - compare : bint + bool """ # GH 18523 diff --git a/pandas/_libs/tslibs/tzconversion.pxd b/pandas/_libs/tslibs/tzconversion.pxd index 9c86057b0a392..c1dd88e5b2313 100644 --- a/pandas/_libs/tslibs/tzconversion.pxd +++ b/pandas/_libs/tslibs/tzconversion.pxd @@ -4,4 +4,5 @@ from numpy cimport int64_t cdef int64_t tz_convert_utc_to_tzlocal(int64_t utc_val, tzinfo tz) cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=*) +cdef int64_t _tz_convert_tzlocal_fromutc(int64_t val, tzinfo tz, bint *fold) cpdef int64_t tz_convert_single(int64_t val, object tz1, object tz2) diff --git a/pandas/_libs/tslibs/tzconversion.pyx b/pandas/_libs/tslibs/tzconversion.pyx index b368f0fde3edc..a9702f91107ec 100644 --- a/pandas/_libs/tslibs/tzconversion.pyx +++ b/pandas/_libs/tslibs/tzconversion.pyx @@ -444,12 +444,12 @@ cdef int64_t[:] _tz_convert_one_way(int64_t[:] vals, object tz, bint to_utc): return converted -cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=True): +cdef inline int64_t _tzlocal_get_offset_components(int64_t val, tzinfo tz, + bint to_utc, + bint *fold=NULL): """ - Convert the i8 representation of a datetime from a tzlocal timezone to - UTC, or vice-versa. - - Private, not intended for use outside of tslibs.conversion + Calculate offset in nanoseconds needed to convert the i8 representation of + a datetime from a tzlocal timezone to UTC, or vice-versa. Parameters ---------- @@ -457,15 +457,22 @@ cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=True): tz : tzinfo to_utc : bint True if converting tzlocal _to_ UTC, False if going the other direction + fold : bint*, default NULL + pointer to fold: whether datetime ends up in a fold or not + after adjustment Returns ------- - result : int64_t + delta : int64_t + + Notes + ----- + Sets fold by pointer """ cdef: npy_datetimestruct dts - int64_t delta datetime dt + int64_t delta dt64_to_dtstruct(val, &dts) dt = datetime(dts.year, dts.month, dts.day, dts.hour, @@ -475,11 +482,69 @@ cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=True): if not to_utc: dt = dt.replace(tzinfo=tzutc()) dt = dt.astimezone(tz) - delta = int(get_utcoffset(tz, dt).total_seconds()) * 1000000000 - if not to_utc: + if fold is not NULL: + fold[0] = dt.fold + + return int(get_utcoffset(tz, dt).total_seconds()) * 1000000000 + + +cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=True): + """ + Convert the i8 representation of a datetime from a tzlocal timezone to + UTC, or vice-versa. + + Private, not intended for use outside of tslibs.conversion + + Parameters + ---------- + val : int64_t + tz : tzinfo + to_utc : bint + True if converting tzlocal _to_ UTC, False if going the other direction + + Returns + ------- + result : int64_t + """ + cdef int64_t delta + + delta = _tzlocal_get_offset_components(val, tz, to_utc, NULL) + + if to_utc: + return val - delta + else: return val + delta - return val - delta + + +cdef int64_t _tz_convert_tzlocal_fromutc(int64_t val, tzinfo tz, bint *fold): + """ + Convert the i8 representation of a datetime from UTC to local timezone, + set fold by pointer + + Private, not intended for use outside of tslibs.conversion + + Parameters + ---------- + val : int64_t + tz : tzinfo + fold : bint* + pointer to fold: whether datetime ends up in a fold or not + after adjustment + + Returns + ------- + result : int64_t + + Notes + ----- + Sets fold by pointer + """ + cdef int64_t delta + + delta = _tzlocal_get_offset_components(val, tz, False, fold) + + return val + delta @cython.boundscheck(False) diff --git a/pandas/_libs/tslibs/util.pxd b/pandas/_libs/tslibs/util.pxd index 936532a81c6d6..e7f6b3334eb65 100644 --- a/pandas/_libs/tslibs/util.pxd +++ b/pandas/_libs/tslibs/util.pxd @@ -42,7 +42,7 @@ cdef extern from "numpy/ndarrayobject.h": bint PyArray_IsIntegerScalar(obj) nogil bint PyArray_Check(obj) nogil -cdef extern from "numpy/npy_common.h": +cdef extern from "numpy/npy_common.h": int64_t NPY_MIN_INT64 diff --git a/pandas/_libs/window/aggregations.pyx b/pandas/_libs/window/aggregations.pyx index f675818599b2c..80b9144042041 100644 --- a/pandas/_libs/window/aggregations.pyx +++ b/pandas/_libs/window/aggregations.pyx @@ -56,7 +56,7 @@ cdef: cdef inline int int_max(int a, int b): return a if a >= b else b cdef inline int int_min(int a, int b): return a if a <= b else b -cdef inline bint is_monotonic_start_end_bounds( +cdef bint is_monotonic_start_end_bounds( ndarray[int64_t, ndim=1] start, ndarray[int64_t, ndim=1] end ): return is_monotonic(start, False)[0] and is_monotonic(end, False)[0] diff --git a/pandas/_libs/writers.pyx b/pandas/_libs/writers.pyx index 73201e75c3c88..9e95dea979577 100644 --- a/pandas/_libs/writers.pyx +++ b/pandas/_libs/writers.pyx @@ -15,8 +15,13 @@ ctypedef fused pandas_string: @cython.boundscheck(False) @cython.wraparound(False) -def write_csv_rows(list data, ndarray data_index, - Py_ssize_t nlevels, ndarray cols, object writer): +def write_csv_rows( + list data, + ndarray data_index, + Py_ssize_t nlevels, + ndarray cols, + object writer +): """ Write the given data to the writer object, pre-allocating where possible for performance improvements. @@ -114,7 +119,9 @@ def convert_json_to_lines(arr: object) -> str: @cython.boundscheck(False) @cython.wraparound(False) def max_len_string_array(pandas_string[:] arr) -> Py_ssize_t: - """ return the maximum size of elements in a 1-dim string array """ + """ + Return the maximum size of elements in a 1-dim string array. + """ cdef: Py_ssize_t i, m = 0, l = 0, length = arr.shape[0] pandas_string val @@ -130,7 +137,9 @@ def max_len_string_array(pandas_string[:] arr) -> Py_ssize_t: cpdef inline Py_ssize_t word_len(object val): - """ return the maximum length of a string or bytes value """ + """ + Return the maximum length of a string or bytes value. + """ cdef: Py_ssize_t l = 0 @@ -148,8 +157,10 @@ cpdef inline Py_ssize_t word_len(object val): @cython.boundscheck(False) @cython.wraparound(False) def string_array_replace_from_nan_rep( - ndarray[object, ndim=1] arr, object nan_rep, - object replace=None): + ndarray[object, ndim=1] arr, + object nan_rep, + object replace=np.nan +): """ Replace the values in the array with 'replacement' if they are 'nan_rep'. Return the same array. @@ -157,9 +168,6 @@ def string_array_replace_from_nan_rep( cdef: Py_ssize_t length = len(arr), i = 0 - if replace is None: - replace = np.nan - for i in range(length): if arr[i] == nan_rep: arr[i] = replace diff --git a/pandas/_testing.py b/pandas/_testing.py index 13af8703cef93..a70f75d6cfaf4 100644 --- a/pandas/_testing.py +++ b/pandas/_testing.py @@ -69,8 +69,8 @@ lzma = _import_lzma() -N = 30 -K = 4 +_N = 30 +_K = 4 _RAISE_NETWORK_ERROR_DEFAULT = False # set testing_mode @@ -743,7 +743,8 @@ def repr_class(x): def assert_attr_equal(attr, left, right, obj="Attributes"): - """checks attributes are equal. Both objects must have attribute. + """ + checks attributes are equal. Both objects must have attribute. Parameters ---------- @@ -805,10 +806,6 @@ def assert_is_valid_plot_return_object(objs): assert isinstance(objs, (plt.Artist, tuple, dict)), msg -def isiterable(obj): - return hasattr(obj, "__iter__") - - def assert_is_sorted(seq): """Assert that the sequence is sorted.""" if isinstance(seq, (Index, Series)): @@ -820,7 +817,8 @@ def assert_is_sorted(seq): def assert_categorical_equal( left, right, check_dtype=True, check_category_order=True, obj="Categorical" ): - """Test that Categoricals are equivalent. + """ + Test that Categoricals are equivalent. Parameters ---------- @@ -860,7 +858,8 @@ def assert_categorical_equal( def assert_interval_array_equal(left, right, exact="equiv", obj="IntervalArray"): - """Test that two IntervalArrays are equivalent. + """ + Test that two IntervalArrays are equivalent. Parameters ---------- @@ -1009,12 +1008,13 @@ def _raise(left, right, err_msg): def assert_extension_array_equal( left, right, check_dtype=True, check_less_precise=False, check_exact=False ): - """Check that left and right ExtensionArrays are equal. + """ + Check that left and right ExtensionArrays are equal. Parameters ---------- left, right : ExtensionArray - The two arrays to compare + The two arrays to compare. check_dtype : bool, default True Whether to check if the ExtensionArray dtypes are identical. check_less_precise : bool or int, default False @@ -1070,6 +1070,7 @@ def assert_series_equal( check_exact=False, check_datetimelike_compat=False, check_categorical=True, + check_category_order=True, obj="Series", ): """ @@ -1104,6 +1105,10 @@ def assert_series_equal( Compare datetime-like which is comparable ignoring dtype. check_categorical : bool, default True Whether to compare internal Categorical exactly. + check_category_order : bool, default True + Whether to compare category order of internal Categoricals + + .. versionadded:: 1.0.2 obj : str, default 'Series' Specify object name being compared, internally used to show appropriate assertion message. @@ -1206,7 +1211,12 @@ def assert_series_equal( if check_categorical: if is_categorical_dtype(left) or is_categorical_dtype(right): - assert_categorical_equal(left.values, right.values, obj=f"{obj} category") + assert_categorical_equal( + left.values, + right.values, + obj=f"{obj} category", + check_category_order=check_category_order, + ) # This could be refactored to use the NDFrame.equals method @@ -1489,7 +1499,8 @@ def assert_sp_array_equal( check_fill_value=True, consolidate_block_indices=False, ): - """Check that the left and right SparseArray are equal. + """ + Check that the left and right SparseArray are equal. Parameters ---------- @@ -1508,7 +1519,6 @@ def assert_sp_array_equal( create a new BlockIndex for that array, with consolidated block indices. """ - _check_isinstance(left, right, pd.arrays.SparseArray) assert_numpy_array_equal(left.sp_values, right.sp_values, check_dtype=check_dtype) @@ -1725,7 +1735,8 @@ def _make_timeseries(start="2000-01-01", end="2000-12-31", freq="1D", seed=None) def all_index_generator(k=10): - """Generator which can be iterated over to get instances of all the various + """ + Generator which can be iterated over to get instances of all the various index classes. Parameters @@ -1764,7 +1775,8 @@ def index_subclass_makers_generator(): def all_timeseries_index_generator(k=10): - """Generator which can be iterated over to get instances of all the classes + """ + Generator which can be iterated over to get instances of all the classes which represent time-series. Parameters @@ -1778,45 +1790,45 @@ def all_timeseries_index_generator(k=10): # make series def makeFloatSeries(name=None): - index = makeStringIndex(N) - return Series(randn(N), index=index, name=name) + index = makeStringIndex(_N) + return Series(randn(_N), index=index, name=name) def makeStringSeries(name=None): - index = makeStringIndex(N) - return Series(randn(N), index=index, name=name) + index = makeStringIndex(_N) + return Series(randn(_N), index=index, name=name) def makeObjectSeries(name=None): - data = makeStringIndex(N) + data = makeStringIndex(_N) data = Index(data, dtype=object) - index = makeStringIndex(N) + index = makeStringIndex(_N) return Series(data, index=index, name=name) def getSeriesData(): - index = makeStringIndex(N) - return {c: Series(randn(N), index=index) for c in getCols(K)} + index = makeStringIndex(_N) + return {c: Series(randn(_N), index=index) for c in getCols(_K)} def makeTimeSeries(nper=None, freq="B", name=None): if nper is None: - nper = N + nper = _N return Series(randn(nper), index=makeDateIndex(nper, freq=freq), name=name) def makePeriodSeries(nper=None, name=None): if nper is None: - nper = N + nper = _N return Series(randn(nper), index=makePeriodIndex(nper), name=name) def getTimeSeriesData(nper=None, freq="B"): - return {c: makeTimeSeries(nper, freq) for c in getCols(K)} + return {c: makeTimeSeries(nper, freq) for c in getCols(_K)} def getPeriodData(nper=None): - return {c: makePeriodSeries(nper) for c in getCols(K)} + return {c: makePeriodSeries(nper) for c in getCols(_K)} # make frame @@ -1855,7 +1867,8 @@ def makePeriodFrame(nper=None): def makeCustomIndex( nentries, nlevels, prefix="#", names=False, ndupe_l=None, idx_type=None ): - """Create an index/multindex with given dimensions, levels, names, etc' + """ + Create an index/multindex with given dimensions, levels, names, etc' nentries - number of entries in index nlevels - number of levels (> 1 produces multindex) @@ -1876,7 +1889,6 @@ def makeCustomIndex( if unspecified, string labels will be generated. """ - if ndupe_l is None: ndupe_l = [1] * nlevels assert is_sequence(ndupe_l) and len(ndupe_l) <= nlevels @@ -1972,35 +1984,39 @@ def makeCustomDataframe( r_idx_type=None, ): """ - nrows, ncols - number of data rows/cols - c_idx_names, idx_names - False/True/list of strings, yields No names , - default names or uses the provided names for the levels of the - corresponding index. You can provide a single string when - c_idx_nlevels ==1. - c_idx_nlevels - number of levels in columns index. > 1 will yield MultiIndex - r_idx_nlevels - number of levels in rows index. > 1 will yield MultiIndex - data_gen_f - a function f(row,col) which return the data value - at that position, the default generator used yields values of the form - "RxCy" based on position. - c_ndupe_l, r_ndupe_l - list of integers, determines the number - of duplicates for each label at a given level of the corresponding - index. The default `None` value produces a multiplicity of 1 across - all levels, i.e. a unique index. Will accept a partial list of length - N < idx_nlevels, for just the first N levels. If ndupe doesn't divide - nrows/ncol, the last label might have lower multiplicity. - dtype - passed to the DataFrame constructor as is, in case you wish to - have more control in conjunction with a custom `data_gen_f` - r_idx_type, c_idx_type - "i"/"f"/"s"/"u"/"dt"/"td". - If idx_type is not None, `idx_nlevels` must be 1. - "i"/"f" creates an integer/float index, - "s"/"u" creates a string/unicode index - "dt" create a datetime index. - "td" create a timedelta index. - - if unspecified, string labels will be generated. + Create a DataFrame using supplied parameters. - Examples: + Parameters + ---------- + nrows, ncols - number of data rows/cols + c_idx_names, idx_names - False/True/list of strings, yields No names , + default names or uses the provided names for the levels of the + corresponding index. You can provide a single string when + c_idx_nlevels ==1. + c_idx_nlevels - number of levels in columns index. > 1 will yield MultiIndex + r_idx_nlevels - number of levels in rows index. > 1 will yield MultiIndex + data_gen_f - a function f(row,col) which return the data value + at that position, the default generator used yields values of the form + "RxCy" based on position. + c_ndupe_l, r_ndupe_l - list of integers, determines the number + of duplicates for each label at a given level of the corresponding + index. The default `None` value produces a multiplicity of 1 across + all levels, i.e. a unique index. Will accept a partial list of length + N < idx_nlevels, for just the first N levels. If ndupe doesn't divide + nrows/ncol, the last label might have lower multiplicity. + dtype - passed to the DataFrame constructor as is, in case you wish to + have more control in conjunction with a custom `data_gen_f` + r_idx_type, c_idx_type - "i"/"f"/"s"/"u"/"dt"/"td". + If idx_type is not None, `idx_nlevels` must be 1. + "i"/"f" creates an integer/float index, + "s"/"u" creates a string/unicode index + "dt" create a datetime index. + "td" create a timedelta index. + + if unspecified, string labels will be generated. + Examples + -------- # 5 row, 3 columns, default names on both, single index on both axis >> makeCustomDataframe(5,3) @@ -2025,7 +2041,6 @@ def makeCustomDataframe( >> a=mkdf(5,3,r_idx_nlevels=2,c_idx_nlevels=4) """ - assert c_idx_nlevels > 0 assert r_idx_nlevels > 0 assert r_idx_type is None or ( @@ -2143,14 +2158,16 @@ def makeMissingDataframe(density=0.9, random_state=None): def optional_args(decorator): - """allows a decorator to take optional positional and keyword arguments. + """ + allows a decorator to take optional positional and keyword arguments. Assumes that taking a single, callable, positional argument means that it is decorating a function, i.e. something like this:: @my_decorator def function(): pass - Calls decorator with decorator(f, *args, **kwargs)""" + Calls decorator with decorator(f, *args, **kwargs) + """ @wraps(decorator) def wrapper(*args, **kwargs): @@ -2214,7 +2231,8 @@ def _get_default_network_errors(): def can_connect(url, error_classes=None): - """Try to connect to the given url. True if succeeds, False if IOError + """ + Try to connect to the given url. True if succeeds, False if IOError raised Parameters @@ -2228,7 +2246,6 @@ def can_connect(url, error_classes=None): Return True if no IOError (unable to connect) or URLError (bad url) was raised """ - if error_classes is None: error_classes = _get_default_network_errors() @@ -2517,7 +2534,6 @@ class RNGContext: Examples -------- - with RNGContext(42): np.random.randn() """ @@ -2584,7 +2600,8 @@ def use_numexpr(use, min_elements=None): def test_parallel(num_threads=2, kwargs_list=None): - """Decorator to run the same function multiple times in parallel. + """ + Decorator to run the same function multiple times in parallel. Parameters ---------- @@ -2593,6 +2610,7 @@ def test_parallel(num_threads=2, kwargs_list=None): kwargs_list : list of dicts, optional The list of kwargs to update original function kwargs on different threads. + Notes ----- This decorator does not pass the return value of the decorated function. @@ -2602,7 +2620,6 @@ def test_parallel(num_threads=2, kwargs_list=None): https://github.com/scikit-image/scikit-image/pull/1519 """ - assert num_threads > 0 has_kwargs_list = kwargs_list is not None if has_kwargs_list: @@ -2673,7 +2690,6 @@ def set_timezone(tz: str): Examples -------- - >>> from datetime import datetime >>> from dateutil.tz import tzlocal >>> tzlocal().tzname(datetime.now()) @@ -2684,7 +2700,6 @@ def set_timezone(tz: str): ... 'EDT' """ - import os import time diff --git a/pandas/compat/chainmap.py b/pandas/compat/chainmap.py index 588bd24ddf797..a84dbb4a661e4 100644 --- a/pandas/compat/chainmap.py +++ b/pandas/compat/chainmap.py @@ -5,7 +5,8 @@ class DeepChainMap(ChainMap[_KT, _VT]): - """Variant of ChainMap that allows direct updates to inner scopes. + """ + Variant of ChainMap that allows direct updates to inner scopes. Only works when all passed mapping are mutable. """ diff --git a/pandas/compat/numpy/function.py b/pandas/compat/numpy/function.py index 05ecccc67daef..ccc970fb453c2 100644 --- a/pandas/compat/numpy/function.py +++ b/pandas/compat/numpy/function.py @@ -99,7 +99,6 @@ def validate_argmin_with_skipna(skipna, args, kwargs): 'skipna' parameter is either an instance of ndarray or is None, since 'skipna' itself should be a boolean """ - skipna, args = process_skipna(skipna, args) validate_argmin(args, kwargs) return skipna @@ -113,7 +112,6 @@ def validate_argmax_with_skipna(skipna, args, kwargs): 'skipna' parameter is either an instance of ndarray or is None, since 'skipna' itself should be a boolean """ - skipna, args = process_skipna(skipna, args) validate_argmax(args, kwargs) return skipna @@ -151,7 +149,6 @@ def validate_argsort_with_ascending(ascending, args, kwargs): either integer type or is None, since 'ascending' itself should be a boolean """ - if is_integer(ascending) or ascending is None: args = (ascending,) + args ascending = True @@ -173,7 +170,6 @@ def validate_clip_with_axis(axis, args, kwargs): so check if the 'axis' parameter is an instance of ndarray, since 'axis' itself should either be an integer or None """ - if isinstance(axis, ndarray): args = (axis,) + args axis = None @@ -298,7 +294,6 @@ def validate_take_with_convert(convert, args, kwargs): ndarray or 'None', so check if the 'convert' parameter is either an instance of ndarray or is None """ - if isinstance(convert, ndarray) or convert is None: args = (convert,) + args convert = True diff --git a/pandas/compat/pickle_compat.py b/pandas/compat/pickle_compat.py index 0a1a1376bfc8d..3f4acca8bce18 100644 --- a/pandas/compat/pickle_compat.py +++ b/pandas/compat/pickle_compat.py @@ -229,7 +229,6 @@ def load(fh, encoding: Optional[str] = None, is_verbose: bool = False): encoding : an optional encoding is_verbose : show exception output """ - try: fh.seek(0) if encoding is not None: diff --git a/pandas/conftest.py b/pandas/conftest.py index 131a011c5a101..be44e6c2b36da 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -17,6 +17,7 @@ from pandas import DataFrame import pandas._testing as tm from pandas.core import ops +from pandas.core.indexes.api import Index, MultiIndex hypothesis.settings.register_profile( "ci", @@ -106,8 +107,8 @@ def axis(request): @pytest.fixture(params=[0, "index"], ids=lambda x: f"axis {repr(x)}") def axis_series(request): """ - Fixture for returning the axis numbers of a Series. - """ + Fixture for returning the axis numbers of a Series. + """ return request.param @@ -118,7 +119,6 @@ def ip(): Will raise a skip if IPython is not installed. """ - pytest.importorskip("IPython", minversion="6.0.0") from IPython.core.interactiveshell import InteractiveShell @@ -441,7 +441,7 @@ def other_closed(request): return request.param -@pytest.fixture(params=[None, np.nan, pd.NaT, float("nan"), np.float("NaN")]) +@pytest.fixture(params=[None, np.nan, pd.NaT, float("nan"), np.float("NaN"), pd.NA]) def nulls_fixture(request): """ Fixture for each null type in pandas. @@ -679,7 +679,6 @@ def any_nullable_int_dtype(request): * 'UInt64' * 'Int64' """ - return request.param @@ -744,6 +743,7 @@ def any_numpy_dtype(request): # categoricals are handled separately _any_skipna_inferred_dtype = [ ("string", ["a", np.nan, "c"]), + ("string", ["a", pd.NA, "c"]), ("bytes", [b"a", np.nan, b"c"]), ("empty", [np.nan, np.nan, np.nan]), ("empty", []), @@ -754,6 +754,7 @@ def any_numpy_dtype(request): ("mixed-integer-float", [1, np.nan, 2.0]), ("decimal", [Decimal(1), np.nan, Decimal(2)]), ("boolean", [True, np.nan, False]), + ("boolean", [True, pd.NA, False]), ("datetime64", [np.datetime64("2013-01-01"), np.nan, np.datetime64("2018-01-01")]), ("datetime", [pd.Timestamp("20130101"), np.nan, pd.Timestamp("20180101")]), ("date", [date(2013, 1, 1), np.nan, date(2018, 1, 1)]), @@ -953,3 +954,106 @@ def __len__(self): return self._data.__len__() return TestNonDictMapping + + +def _gen_mi(): + # a MultiIndex used to test the general functionality of this object + + # See Also: tests.multi.conftest.idx + major_axis = Index(["foo", "bar", "baz", "qux"]) + minor_axis = Index(["one", "two"]) + + major_codes = np.array([0, 0, 1, 2, 3, 3]) + minor_codes = np.array([0, 1, 0, 1, 0, 1]) + index_names = ["first", "second"] + mi = MultiIndex( + levels=[major_axis, minor_axis], + codes=[major_codes, minor_codes], + names=index_names, + verify_integrity=False, + ) + return mi + + +indices_dict = { + "unicode": tm.makeUnicodeIndex(100), + "string": tm.makeStringIndex(100), + "datetime": tm.makeDateIndex(100), + "datetime-tz": tm.makeDateIndex(100, tz="US/Pacific"), + "period": tm.makePeriodIndex(100), + "timedelta": tm.makeTimedeltaIndex(100), + "int": tm.makeIntIndex(100), + "uint": tm.makeUIntIndex(100), + "range": tm.makeRangeIndex(100), + "float": tm.makeFloatIndex(100), + "bool": tm.makeBoolIndex(10), + "categorical": tm.makeCategoricalIndex(100), + "interval": tm.makeIntervalIndex(100), + "empty": Index([]), + "tuples": MultiIndex.from_tuples(zip(["foo", "bar", "baz"], [1, 2, 3])), + "multi": _gen_mi(), + "repeats": Index([0, 0, 1, 1, 2, 2]), +} + + +@pytest.fixture(params=indices_dict.keys()) +def indices(request): + """ + Fixture for many "simple" kinds of indices. + + These indices are unlikely to cover corner cases, e.g. + - no names + - no NaTs/NaNs + - no values near implementation bounds + - ... + """ + # copy to avoid mutation, e.g. setting .name + return indices_dict[request.param].copy() + + +def _create_series(index): + """ Helper for the _series dict """ + size = len(index) + data = np.random.randn(size) + return pd.Series(data, index=index, name="a") + + +_series = { + f"series-with-{index_id}-index": _create_series(index) + for index_id, index in indices_dict.items() +} + + +@pytest.fixture +def series_with_simple_index(indices): + """ + Fixture for tests on series with changing types of indices. + """ + return _create_series(indices) + + +_narrow_dtypes = [ + np.float16, + np.float32, + np.int8, + np.int16, + np.int32, + np.uint8, + np.uint16, + np.uint32, +] +_narrow_series = { + f"{dtype.__name__}-series": tm.makeFloatSeries(name="a").astype(dtype) + for dtype in _narrow_dtypes +} + +_index_or_series_objs = {**indices_dict, **_series, **_narrow_series} + + +@pytest.fixture(params=_index_or_series_objs.keys()) +def index_or_series_obj(request): + """ + Fixture for tests on indexes, series and series with a narrow dtype + copy to avoid mutation, e.g. setting .name + """ + return _index_or_series_objs[request.param].copy(deep=True) diff --git a/pandas/core/accessor.py b/pandas/core/accessor.py index a04e9c3e68310..fc40f1db1918a 100644 --- a/pandas/core/accessor.py +++ b/pandas/core/accessor.py @@ -7,7 +7,7 @@ from typing import FrozenSet, Set import warnings -from pandas.util._decorators import Appender +from pandas.util._decorators import doc class DirNamesMixin: @@ -193,98 +193,96 @@ def __get__(self, obj, cls): return accessor_obj +@doc(klass="", others="") def _register_accessor(name, cls): - def decorator(accessor): - if hasattr(cls, name): - warnings.warn( - f"registration of accessor {repr(accessor)} under name " - f"{repr(name)} for type {repr(cls)} is overriding a preexisting" - f"attribute with the same name.", - UserWarning, - stacklevel=2, - ) - setattr(cls, name, CachedAccessor(name, accessor)) - cls._accessors.add(name) - return accessor - - return decorator - + """ + Register a custom accessor on {klass} objects. -_doc = """ -Register a custom accessor on %(klass)s objects. + Parameters + ---------- + name : str + Name under which the accessor should be registered. A warning is issued + if this name conflicts with a preexisting attribute. -Parameters ----------- -name : str - Name under which the accessor should be registered. A warning is issued - if this name conflicts with a preexisting attribute. + Returns + ------- + callable + A class decorator. -Returns -------- -callable - A class decorator. + See Also + -------- + {others} -See Also --------- -%(others)s + Notes + ----- + When accessed, your accessor will be initialized with the pandas object + the user is interacting with. So the signature must be -Notes ------ -When accessed, your accessor will be initialized with the pandas object -the user is interacting with. So the signature must be + .. code-block:: python -.. code-block:: python + def __init__(self, pandas_object): # noqa: E999 + ... - def __init__(self, pandas_object): # noqa: E999 - ... + For consistency with pandas methods, you should raise an ``AttributeError`` + if the data passed to your accessor has an incorrect dtype. -For consistency with pandas methods, you should raise an ``AttributeError`` -if the data passed to your accessor has an incorrect dtype. + >>> pd.Series(['a', 'b']).dt + Traceback (most recent call last): + ... + AttributeError: Can only use .dt accessor with datetimelike values ->>> pd.Series(['a', 'b']).dt -Traceback (most recent call last): -... -AttributeError: Can only use .dt accessor with datetimelike values + Examples + -------- + In your library code:: -Examples --------- + import pandas as pd -In your library code:: + @pd.api.extensions.register_dataframe_accessor("geo") + class GeoAccessor: + def __init__(self, pandas_obj): + self._obj = pandas_obj - import pandas as pd + @property + def center(self): + # return the geographic center point of this DataFrame + lat = self._obj.latitude + lon = self._obj.longitude + return (float(lon.mean()), float(lat.mean())) - @pd.api.extensions.register_dataframe_accessor("geo") - class GeoAccessor: - def __init__(self, pandas_obj): - self._obj = pandas_obj + def plot(self): + # plot this array's data on a map, e.g., using Cartopy + pass - @property - def center(self): - # return the geographic center point of this DataFrame - lat = self._obj.latitude - lon = self._obj.longitude - return (float(lon.mean()), float(lat.mean())) + Back in an interactive IPython session: - def plot(self): - # plot this array's data on a map, e.g., using Cartopy - pass + >>> ds = pd.DataFrame({{'longitude': np.linspace(0, 10), + ... 'latitude': np.linspace(0, 20)}}) + >>> ds.geo.center + (5.0, 10.0) + >>> ds.geo.plot() + # plots data on a map + """ -Back in an interactive IPython session: + def decorator(accessor): + if hasattr(cls, name): + warnings.warn( + f"registration of accessor {repr(accessor)} under name " + f"{repr(name)} for type {repr(cls)} is overriding a preexisting" + f"attribute with the same name.", + UserWarning, + stacklevel=2, + ) + setattr(cls, name, CachedAccessor(name, accessor)) + cls._accessors.add(name) + return accessor - >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10), - ... 'latitude': np.linspace(0, 20)}) - >>> ds.geo.center - (5.0, 10.0) - >>> ds.geo.plot() - # plots data on a map -""" + return decorator -@Appender( - _doc - % dict( - klass="DataFrame", others=("register_series_accessor, register_index_accessor") - ) +@doc( + _register_accessor, + klass="DataFrame", + others="register_series_accessor, register_index_accessor", ) def register_dataframe_accessor(name): from pandas import DataFrame @@ -292,11 +290,10 @@ def register_dataframe_accessor(name): return _register_accessor(name, DataFrame) -@Appender( - _doc - % dict( - klass="Series", others=("register_dataframe_accessor, register_index_accessor") - ) +@doc( + _register_accessor, + klass="Series", + others="register_dataframe_accessor, register_index_accessor", ) def register_series_accessor(name): from pandas import Series @@ -304,11 +301,10 @@ def register_series_accessor(name): return _register_accessor(name, Series) -@Appender( - _doc - % dict( - klass="Index", others=("register_dataframe_accessor, register_series_accessor") - ) +@doc( + _register_accessor, + klass="Index", + others="register_dataframe_accessor, register_series_accessor", ) def register_index_accessor(name): from pandas import Index diff --git a/pandas/core/aggregation.py b/pandas/core/aggregation.py index 79b87f146b9a7..448f84d58d7a0 100644 --- a/pandas/core/aggregation.py +++ b/pandas/core/aggregation.py @@ -98,7 +98,8 @@ def normalize_keyword_aggregation(kwargs: dict) -> Tuple[dict, List[str], List[i def _make_unique_kwarg_list( seq: Sequence[Tuple[Any, Any]] ) -> Sequence[Tuple[Any, Any]]: - """Uniquify aggfunc name of the pairs in the order list + """ + Uniquify aggfunc name of the pairs in the order list Examples: -------- diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 886b0a3c5fec1..02a979aea6c6b 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -11,7 +11,7 @@ from pandas._libs import Timestamp, algos, hashtable as htable, lib from pandas._libs.tslib import iNaT -from pandas.util._decorators import Appender, Substitution +from pandas.util._decorators import doc from pandas.core.dtypes.cast import ( construct_1d_object_array_from_listlike, @@ -85,7 +85,6 @@ def _ensure_data(values, dtype=None): values : ndarray pandas_dtype : str or dtype """ - # we check some simple dtypes first if is_object_dtype(dtype): return ensure_object(np.asarray(values)), "object" @@ -182,7 +181,6 @@ def _reconstruct_data(values, dtype, original): ------- Index for extension types, otherwise ndarray casted to dtype """ - if is_extension_array_dtype(dtype): values = dtype.construct_array_type()._from_sequence(values) elif is_bool_dtype(dtype): @@ -368,7 +366,6 @@ def unique(values): >>> pd.unique([('a', 'b'), ('b', 'a'), ('a', 'c'), ('b', 'a')]) array([('a', 'b'), ('b', 'a'), ('a', 'c')], dtype=object) """ - values = _ensure_arraylike(values) if is_extension_array_dtype(values): @@ -487,9 +484,32 @@ def _factorize_array( return codes, uniques -_shared_docs[ - "factorize" -] = """ +@doc( + values=dedent( + """\ + values : sequence + A 1-D sequence. Sequences that aren't pandas objects are + coerced to ndarrays before factorization. + """ + ), + sort=dedent( + """\ + sort : bool, default False + Sort `uniques` and shuffle `codes` to maintain the + relationship. + """ + ), + size_hint=dedent( + """\ + size_hint : int, optional + Hint to the hashtable sizer. + """ + ), +) +def factorize( + values, sort: bool = False, na_sentinel: int = -1, size_hint: Optional[int] = None +) -> Tuple[np.ndarray, Union[np.ndarray, ABCIndex]]: + """ Encode the object as an enumerated type or categorical variable. This method is useful for obtaining a numeric representation of an @@ -499,10 +519,10 @@ def _factorize_array( Parameters ---------- - %(values)s%(sort)s + {values}{sort} na_sentinel : int, default -1 Value to mark "not found". - %(size_hint)s\ + {size_hint}\ Returns ------- @@ -580,34 +600,6 @@ def _factorize_array( >>> uniques Index(['a', 'c'], dtype='object') """ - - -@Substitution( - values=dedent( - """\ - values : sequence - A 1-D sequence. Sequences that aren't pandas objects are - coerced to ndarrays before factorization. - """ - ), - sort=dedent( - """\ - sort : bool, default False - Sort `uniques` and shuffle `codes` to maintain the - relationship. - """ - ), - size_hint=dedent( - """\ - size_hint : int, optional - Hint to the hashtable sizer. - """ - ), -) -@Appender(_shared_docs["factorize"]) -def factorize( - values, sort: bool = False, na_sentinel: int = -1, size_hint: Optional[int] = None -) -> Tuple[np.ndarray, Union[np.ndarray, ABCIndex]]: # Implementation notes: This method is responsible for 3 things # 1.) coercing data to array-like (ndarray, Index, extension array) # 2.) factorizing codes and uniques @@ -796,7 +788,6 @@ def duplicated(values, keep="first") -> np.ndarray: ------- duplicated : ndarray """ - values, _ = _ensure_data(values) ndtype = values.dtype.name f = getattr(htable, f"duplicated_{ndtype}") diff --git a/pandas/core/apply.py b/pandas/core/apply.py index 81e1d84880f60..70e0a129c055f 100644 --- a/pandas/core/apply.py +++ b/pandas/core/apply.py @@ -35,7 +35,6 @@ def frame_apply( kwds=None, ): """ construct and return a row or column based frame apply object """ - axis = obj._get_axis_number(axis) klass: Type[FrameApply] if axis == 0: @@ -144,7 +143,6 @@ def agg_axis(self) -> "Index": def get_result(self): """ compute the results """ - # dispatch to agg if is_list_like(self.f) or is_dict_like(self.f): return self.obj.aggregate(self.f, axis=self.axis, *self.args, **self.kwds) @@ -193,7 +191,6 @@ def apply_empty_result(self): we will try to apply the function to an empty series in order to see if this is a reduction function """ - # we are not asked to reduce or infer reduction # so just return a copy of the existing object if self.result_type not in ["reduce", None]: @@ -396,7 +393,6 @@ def wrap_results_for_axis( self, results: ResType, res_index: "Index" ) -> "DataFrame": """ return the results for the rows """ - result = self.obj._constructor(data=results) if not isinstance(results[0], ABCSeries): @@ -457,7 +453,6 @@ def wrap_results_for_axis( def infer_to_same_shape(self, results: ResType, res_index: "Index") -> "DataFrame": """ infer the results to the same shape as the input object """ - result = self.obj._constructor(data=results) result = result.T diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index c3c91cea43f6b..b5da6d4c11616 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -1,4 +1,5 @@ -"""An interface for extending pandas with custom arrays. +""" +An interface for extending pandas with custom arrays. .. warning:: @@ -213,7 +214,8 @@ def _from_sequence(cls, scalars, dtype=None, copy=False): @classmethod def _from_sequence_of_strings(cls, strings, dtype=None, copy=False): - """Construct a new ExtensionArray from a sequence of strings. + """ + Construct a new ExtensionArray from a sequence of strings. .. versionadded:: 0.24.0 @@ -961,7 +963,8 @@ def __repr__(self) -> str: return f"{class_name}{data}\nLength: {len(self)}, dtype: {self.dtype}" def _formatter(self, boxed: bool = False) -> Callable[[Any], Optional[str]]: - """Formatting function for scalar values. + """ + Formatting function for scalar values. This is used in the default '__repr__'. The returned formatting function receives instances of your scalar type. diff --git a/pandas/core/arrays/boolean.py b/pandas/core/arrays/boolean.py index db62136947250..d93b5fbc83312 100644 --- a/pandas/core/arrays/boolean.py +++ b/pandas/core/arrays/boolean.py @@ -1,10 +1,11 @@ import numbers -from typing import TYPE_CHECKING, Any, List, Tuple, Type, Union +from typing import TYPE_CHECKING, List, Tuple, Type, Union import warnings import numpy as np from pandas._libs import lib, missing as libmissing +from pandas._typing import ArrayLike from pandas.compat import set_function_name from pandas.compat.numpy import function as nv @@ -281,20 +282,15 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): if not mask.ndim == 1: raise ValueError("mask must be a 1D array") - if copy: - values = values.copy() - mask = mask.copy() - - self._data = values - self._mask = mask self._dtype = BooleanDtype() + super().__init__(values, mask, copy=copy) @property - def dtype(self): + def dtype(self) -> BooleanDtype: return self._dtype @classmethod - def _from_sequence(cls, scalars, dtype=None, copy: bool = False): + def _from_sequence(cls, scalars, dtype=None, copy: bool = False) -> "BooleanArray": if dtype: assert dtype == "boolean" values, mask = coerce_to_array(scalars, copy=copy) @@ -303,7 +299,7 @@ def _from_sequence(cls, scalars, dtype=None, copy: bool = False): @classmethod def _from_sequence_of_strings( cls, strings: List[str], dtype=None, copy: bool = False - ): + ) -> "BooleanArray": def map_string(s): if isna(s): return s @@ -317,18 +313,18 @@ def map_string(s): scalars = [map_string(x) for x in strings] return cls._from_sequence(scalars, dtype, copy) - def _values_for_factorize(self) -> Tuple[np.ndarray, Any]: + def _values_for_factorize(self) -> Tuple[np.ndarray, int]: data = self._data.astype("int8") data[self._mask] = -1 return data, -1 @classmethod - def _from_factorized(cls, values, original: "BooleanArray"): + def _from_factorized(cls, values, original: "BooleanArray") -> "BooleanArray": return cls._from_sequence(values, dtype=original.dtype) _HANDLED_TYPES = (np.ndarray, numbers.Number, bool, np.bool_) - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs): # For BooleanArray inputs, we apply the ufunc to ._data # and mask the result. if method == "reduce": @@ -373,7 +369,7 @@ def reconstruct(x): else: return reconstruct(result) - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: _is_scalar = is_scalar(value) if _is_scalar: value = [value] @@ -387,7 +383,7 @@ def __setitem__(self, key, value): self._data[key] = value self._mask[key] = mask - def astype(self, dtype, copy=True): + def astype(self, dtype, copy: bool = True) -> ArrayLike: """ Cast to a NumPy array or ExtensionArray with 'dtype'. @@ -402,8 +398,8 @@ def astype(self, dtype, copy=True): Returns ------- - array : ndarray or ExtensionArray - NumPy ndarray, BooleanArray or IntergerArray with 'dtype' for its dtype. + ndarray or ExtensionArray + NumPy ndarray, BooleanArray or IntegerArray with 'dtype' for its dtype. Raises ------ @@ -492,7 +488,6 @@ def any(self, skipna: bool = True, **kwargs): Examples -------- - The result indicates whether any element is True (and by default skips NAs): @@ -561,7 +556,6 @@ def all(self, skipna: bool = True, **kwargs): Examples -------- - The result indicates whether any element is True (and by default skips NAs): @@ -693,7 +687,7 @@ def cmp_method(self, other): name = f"__{op.__name__}" return set_function_name(cmp_method, name, cls) - def _reduce(self, name, skipna=True, **kwargs): + def _reduce(self, name: str, skipna: bool = True, **kwargs): if name in {"any", "all"}: return getattr(self, name)(skipna=skipna, **kwargs) @@ -722,7 +716,7 @@ def _reduce(self, name, skipna=True, **kwargs): return result - def _maybe_mask_result(self, result, mask, other, op_name): + def _maybe_mask_result(self, result, mask, other, op_name: str): """ Parameters ---------- diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index d26ff7490e714..a5048e3aae899 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -341,10 +341,7 @@ def __init__( values = _convert_to_list_like(values) # By convention, empty lists result in object dtype: - if len(values) == 0: - sanitize_dtype = "object" - else: - sanitize_dtype = None + sanitize_dtype = "object" if len(values) == 0 else None null_mask = isna(values) if null_mask.any(): values = [values[idx] for idx in np.where(~null_mask)[0]] @@ -644,7 +641,13 @@ def from_codes(cls, codes, categories=None, ordered=None, dtype=None): ) raise ValueError(msg) - codes = np.asarray(codes) # #21767 + if is_extension_array_dtype(codes) and is_integer_dtype(codes): + # Avoid the implicit conversion of Int to object + if isna(codes).any(): + raise ValueError("codes cannot contain NA values") + codes = codes.to_numpy(dtype=np.int64) + else: + codes = np.asarray(codes) if len(codes) and not is_integer_dtype(codes): raise ValueError("codes need to be array-like integers") @@ -695,7 +698,6 @@ def _set_categories(self, categories, fastpath=False): [a, c] Categories (2, object): [a, c] """ - if fastpath: new_dtype = CategoricalDtype._from_fastpath(categories, self.ordered) else: @@ -1221,7 +1223,6 @@ def shape(self): ------- shape : tuple """ - return tuple([len(self._codes)]) def shift(self, periods, fill_value=None): @@ -1378,7 +1379,6 @@ def isna(self): Categorical.notna : Boolean inverse of Categorical.isna. """ - ret = self._codes == -1 return ret @@ -1493,7 +1493,7 @@ def check_for_ordered(self, op): def _values_for_argsort(self): return self._codes.copy() - def argsort(self, ascending=True, kind="quicksort", *args, **kwargs): + def argsort(self, ascending=True, kind="quicksort", **kwargs): """ Return the indices that would sort the Categorical. @@ -1508,7 +1508,7 @@ def argsort(self, ascending=True, kind="quicksort", *args, **kwargs): or descending sort. kind : {'quicksort', 'mergesort', 'heapsort'}, optional Sorting algorithm. - *args, **kwargs: + **kwargs: passed through to :func:`numpy.argsort`. Returns @@ -1544,7 +1544,7 @@ def argsort(self, ascending=True, kind="quicksort", *args, **kwargs): >>> cat.argsort() array([2, 0, 1]) """ - return super().argsort(ascending=ascending, kind=kind, *args, **kwargs) + return super().argsort(ascending=ascending, kind=kind, **kwargs) def sort_values(self, inplace=False, ascending=True, na_position="last"): """ @@ -1888,7 +1888,8 @@ def __contains__(self, key) -> bool: return contains(self, key, container=self._codes) def _tidy_repr(self, max_vals=10, footer=True) -> str: - """ a short repr displaying only max_vals and an optional (but default + """ + a short repr displaying only max_vals and an optional (but default footer) """ num = max_vals // 2 @@ -1928,7 +1929,6 @@ def _repr_categories_info(self) -> str: """ Returns a string representation of the footer. """ - category_strs = self._repr_categories() dtype = str(self.categories.dtype) levheader = f"Categories ({len(self.categories)}, {dtype}): " @@ -2254,7 +2254,6 @@ def unique(self): Series.unique """ - # unlike np.unique, unique1d does not sort unique_codes = unique1d(self.codes) cat = self.copy() @@ -2314,7 +2313,6 @@ def is_dtype_equal(self, other): ------- bool """ - try: return hash(self.dtype) == hash(other.dtype) except (AttributeError, TypeError): @@ -2388,7 +2386,6 @@ def isin(self, values): Examples -------- - >>> s = pd.Categorical(['lama', 'cow', 'lama', 'beetle', 'lama', ... 'hippo']) >>> s.isin(['cow', 'lama']) @@ -2441,18 +2438,30 @@ def replace(self, to_replace, value, inplace: bool = False): """ inplace = validate_bool_kwarg(inplace, "inplace") cat = self if inplace else self.copy() - if to_replace in cat.categories: - if isna(value): - cat.remove_categories(to_replace, inplace=True) - else: + + # build a dict of (to replace -> value) pairs + if is_list_like(to_replace): + # if to_replace is list-like and value is scalar + replace_dict = {replace_value: value for replace_value in to_replace} + else: + # if both to_replace and value are scalar + replace_dict = {to_replace: value} + + # other cases, like if both to_replace and value are list-like or if + # to_replace is a dict, are handled separately in NDFrame + for replace_value, new_value in replace_dict.items(): + if replace_value in cat.categories: + if isna(new_value): + cat.remove_categories(replace_value, inplace=True) + continue categories = cat.categories.tolist() - index = categories.index(to_replace) - if value in cat.categories: - value_index = categories.index(value) + index = categories.index(replace_value) + if new_value in cat.categories: + value_index = categories.index(new_value) cat._codes[cat._codes == index] = value_index - cat.remove_categories(to_replace, inplace=True) + cat.remove_categories(replace_value, inplace=True) else: - categories[index] = value + categories[index] = new_value cat.rename_categories(categories, inplace=True) if not inplace: return cat diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 4f14ac2a14157..f637e16caa4c6 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -134,7 +134,8 @@ def _simple_new(cls, values, **kwargs): @property def _scalar_type(self) -> Type[DatetimeLikeScalar]: - """The scalar associated with this datelike + """ + The scalar associated with this datelike * PeriodArray : Period * DatetimeArray : Timestamp @@ -500,7 +501,6 @@ def __getitem__(self, key): This getitem defers to the underlying array, which by-definition can only handle list-likes, slices, and integer scalars """ - is_int = lib.is_integer(key) if lib.is_scalar(key) and not is_int: raise IndexError( @@ -520,7 +520,9 @@ def __getitem__(self, key): if com.is_bool_indexer(key): # first convert to boolean, because check_array_indexer doesn't # allow object dtype - key = np.asarray(key, dtype=bool) + if is_object_dtype(key): + key = np.asarray(key, dtype=bool) + key = check_array_indexer(self, key) if key.all(): key = slice(0, None, None) @@ -702,12 +704,31 @@ def take(self, indices, allow_fill=False, fill_value=None): @classmethod def _concat_same_type(cls, to_concat): - dtypes = {x.dtype for x in to_concat} - assert len(dtypes) == 1 - dtype = list(dtypes)[0] + + # do not pass tz to set because tzlocal cannot be hashed + dtypes = {str(x.dtype) for x in to_concat} + if len(dtypes) != 1: + raise ValueError("to_concat must have the same dtype (tz)", dtypes) + + obj = to_concat[0] + dtype = obj.dtype values = np.concatenate([x.asi8 for x in to_concat]) - return cls(values, dtype=dtype) + + if is_period_dtype(to_concat[0].dtype): + new_freq = obj.freq + else: + # GH 3232: If the concat result is evenly spaced, we can retain the + # original frequency + new_freq = None + to_concat = [x for x in to_concat if len(x)] + + if obj.freq is not None and all(x.freq == obj.freq for x in to_concat): + pairs = zip(to_concat[:-1], to_concat[1:]) + if all(pair[0][-1] + obj.freq == pair[1][0] for pair in pairs): + new_freq = obj.freq + + return cls._simple_new(values, dtype=dtype, freq=new_freq) def copy(self): values = self.asi8.copy() @@ -756,8 +777,10 @@ def searchsorted(self, value, side="left", sorter=None): if isinstance(value, str): try: value = self._scalar_from_string(value) - except ValueError: - raise TypeError("searchsorted requires compatible dtype or scalar") + except ValueError as e: + raise TypeError( + "searchsorted requires compatible dtype or scalar" + ) from e elif is_valid_nat_for_dtype(value, self.dtype): value = NaT @@ -873,7 +896,6 @@ def _maybe_mask_results(self, result, fill_value=iNaT, convert=None): This is an internal routine. """ - if self._hasnans: if convert: result = result.astype(convert) @@ -1021,7 +1043,7 @@ def _validate_frequency(cls, index, freq, **kwargs): raise ValueError( f"Inferred frequency {inferred} from passed values " f"does not conform to passed frequency {freq.freqstr}" - ) + ) from e # monotonicity/uniqueness properties are called via frequencies.infer_freq, # see GH#23789 diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 5888600d2fa8e..a75536e46e60d 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -283,7 +283,8 @@ def __init__(self, values, dtype=_NS_DTYPE, freq=None, copy=False): @classmethod def _simple_new(cls, values, freq=None, dtype=_NS_DTYPE): assert isinstance(values, np.ndarray) - if values.dtype == "i8": + if values.dtype != _NS_DTYPE: + assert values.dtype == "i8" values = values.view(_NS_DTYPE) result = object.__new__(cls) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 4bfd5f5770b69..f1e0882def13b 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -1,10 +1,11 @@ import numbers -from typing import TYPE_CHECKING, Any, Dict, Tuple, Type, Union +from typing import TYPE_CHECKING, Tuple, Type, Union import warnings import numpy as np from pandas._libs import lib, missing as libmissing +from pandas._typing import ArrayLike from pandas.compat import set_function_name from pandas.util._decorators import cache_readonly @@ -102,6 +103,10 @@ def __from_arrow__( import pyarrow # noqa: F811 from pandas.core.arrays._arrow_utils import pyarrow_array_to_numpy_and_mask + pyarrow_type = pyarrow.from_numpy_dtype(self.type) + if not array.type.equals(pyarrow_type): + array = array.cast(pyarrow_type) + if isinstance(array, pyarrow.Array): chunks = [array] else: @@ -147,7 +152,6 @@ def safe_cast(values, dtype, copy: bool): ints. """ - try: return values.astype(dtype, casting="safe", copy=copy) except TypeError: @@ -347,13 +351,7 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): "mask should be boolean numpy array. Use " "the 'integer_array' function instead" ) - - if copy: - values = values.copy() - mask = mask.copy() - - self._data = values - self._mask = mask + super().__init__(values, mask, copy=copy) @classmethod def _from_sequence(cls, scalars, dtype=None, copy: bool = False) -> "IntegerArray": @@ -417,7 +415,7 @@ def reconstruct(x): else: return reconstruct(result) - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: _is_scalar = is_scalar(value) if _is_scalar: value = [value] @@ -431,9 +429,9 @@ def __setitem__(self, key, value): self._data[key] = value self._mask[key] = mask - def astype(self, dtype, copy=True): + def astype(self, dtype, copy: bool = True) -> ArrayLike: """ - Cast to a NumPy array or IntegerArray with 'dtype'. + Cast to a NumPy array or ExtensionArray with 'dtype'. Parameters ---------- @@ -446,8 +444,8 @@ def astype(self, dtype, copy=True): Returns ------- - array : ndarray or IntegerArray - NumPy ndarray or IntergerArray with 'dtype' for its dtype. + ndarray or ExtensionArray + NumPy ndarray, BooleanArray or IntegerArray with 'dtype' for its dtype. Raises ------ @@ -479,7 +477,8 @@ def astype(self, dtype, copy=True): @property def _ndarray_values(self) -> np.ndarray: - """Internal pandas method for lossy conversion to a NumPy ndarray. + """ + Internal pandas method for lossy conversion to a NumPy ndarray. This method is not part of the pandas interface. @@ -488,13 +487,14 @@ def _ndarray_values(self) -> np.ndarray: """ return self._data - def _values_for_factorize(self) -> Tuple[np.ndarray, Any]: + def _values_for_factorize(self) -> Tuple[np.ndarray, float]: # TODO: https://github.com/pandas-dev/pandas/issues/30037 # use masked algorithms, rather than object-dtype / np.nan. return self.to_numpy(na_value=np.nan), np.nan def _values_for_argsort(self) -> np.ndarray: - """Return values for sorting. + """ + Return values for sorting. Returns ------- @@ -565,7 +565,7 @@ def cmp_method(self, other): name = f"__{op.__name__}__" return set_function_name(cmp_method, name, cls) - def _reduce(self, name, skipna=True, **kwargs): + def _reduce(self, name: str, skipna: bool = True, **kwargs): data = self._data mask = self._mask @@ -592,7 +592,7 @@ def _reduce(self, name, skipna=True, **kwargs): return result - def _maybe_mask_result(self, result, mask, other, op_name): + def _maybe_mask_result(self, result, mask, other, op_name: str): """ Parameters ---------- @@ -601,7 +601,6 @@ def _maybe_mask_result(self, result, mask, other, op_name): other : scalar or array-like op_name : str """ - # if we have a float operand we are by-definition # a float result # or our op is a divide @@ -768,7 +767,7 @@ class UInt64Dtype(_IntegerDtype): __doc__ = _dtype_docstring.format(dtype="uint64") -_dtypes: Dict[str, _IntegerDtype] = { +_dtypes = { "int8": Int8Dtype(), "int16": Int16Dtype(), "int32": Int32Dtype(), diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 0b35a031bc53f..f5167f470b056 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -460,7 +460,8 @@ def from_tuples(cls, data, closed="right", copy=False, dtype=None): return cls.from_arrays(left, right, closed, copy=False, dtype=dtype) def _validate(self): - """Verify that the IntervalArray is valid. + """ + Verify that the IntervalArray is valid. Checks that @@ -724,45 +725,18 @@ def _concat_same_type(cls, to_concat): right = np.concatenate([interval.right for interval in to_concat]) return cls._simple_new(left, right, closed=closed, copy=False) - def _shallow_copy(self, left=None, right=None, closed=None): + def _shallow_copy(self, left, right): """ Return a new IntervalArray with the replacement attributes Parameters ---------- - left : array-like + left : Index Values to be used for the left-side of the intervals. - If None, the existing left and right values will be used. - - right : array-like + right : Index Values to be used for the right-side of the intervals. - If None and left is IntervalArray-like, the left and right - of the IntervalArray-like will be used. - - closed : {'left', 'right', 'both', 'neither'}, optional - Whether the intervals are closed on the left-side, right-side, both - or neither. If None, the existing closed will be used. """ - if left is None: - - # no values passed - left, right = self.left, self.right - - elif right is None: - - # only single value passed, could be an IntervalArray - # or array of Intervals - if not isinstance(left, (type(self), ABCIntervalIndex)): - left = type(self)(left) - - left, right = left.left, left.right - else: - - # both left and right are values - pass - - closed = closed or self.closed - return self._simple_new(left, right, closed=closed, verify_integrity=False) + return self._simple_new(left, right, closed=self.closed, verify_integrity=False) def copy(self): """ @@ -1034,7 +1008,9 @@ def set_closed(self, closed): msg = f"invalid option for 'closed': {closed}" raise ValueError(msg) - return self._shallow_copy(closed=closed) + return type(self)._simple_new( + left=self.left, right=self.right, closed=closed, verify_integrity=False + ) @property def length(self): @@ -1126,8 +1102,8 @@ def __arrow_array__(self, type=None): subtype = pyarrow.from_numpy_dtype(self.dtype.subtype) except TypeError: raise TypeError( - "Conversion to arrow with subtype '{}' " - "is not supported".format(self.dtype.subtype) + f"Conversion to arrow with subtype '{self.dtype.subtype}' " + "is not supported" ) interval_type = ArrowIntervalType(subtype, self.closed) storage_array = pyarrow.StructArray.from_arrays( @@ -1156,14 +1132,12 @@ def __arrow_array__(self, type=None): if not type.equals(interval_type): raise TypeError( "Not supported to convert IntervalArray to type with " - "different 'subtype' ({0} vs {1}) and 'closed' ({2} vs {3}) " - "attributes".format( - self.dtype.subtype, type.subtype, self.closed, type.closed - ) + f"different 'subtype' ({self.dtype.subtype} vs {type.subtype}) " + f"and 'closed' ({self.closed} vs {type.closed}) attributes" ) else: raise TypeError( - "Not supported to convert IntervalArray to '{0}' type".format(type) + f"Not supported to convert IntervalArray to '{type}' type" ) return pyarrow.ExtensionArray.from_storage(interval_type, storage_array) @@ -1175,7 +1149,7 @@ def __arrow_array__(self, type=None): Parameters ---------- - na_tuple : boolean, default True + na_tuple : bool, default True Returns NA as a tuple if True, ``(nan, nan)``, or just as the NA value itself if False, ``nan``. diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index 80e317123126a..47892b55b3ce8 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Type, TypeVar import numpy as np from pandas._libs import lib, missing as libmissing +from pandas._typing import Scalar from pandas.core.dtypes.common import is_integer, is_object_dtype, is_string_dtype from pandas.core.dtypes.missing import isna, notna @@ -12,7 +13,10 @@ from pandas.core.indexers import check_array_indexer if TYPE_CHECKING: - from pandas._typing import Scalar + from pandas import Series + + +BaseMaskedArrayT = TypeVar("BaseMaskedArrayT", bound="BaseMaskedArray") class BaseMaskedArray(ExtensionArray, ExtensionOpsMixin): @@ -22,11 +26,16 @@ class BaseMaskedArray(ExtensionArray, ExtensionOpsMixin): numpy based """ - _data: np.ndarray - _mask: np.ndarray - # The value used to fill '_data' to avoid upcasting - _internal_fill_value: "Scalar" + _internal_fill_value: Scalar + + def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): + if copy: + values = values.copy() + mask = mask.copy() + + self._data = values + self._mask = mask def __getitem__(self, item): if is_integer(item): @@ -48,12 +57,12 @@ def __iter__(self): def __len__(self) -> int: return len(self._data) - def __invert__(self): + def __invert__(self: BaseMaskedArrayT) -> BaseMaskedArrayT: return type(self)(~self._data, self._mask) def to_numpy( - self, dtype=None, copy=False, na_value: "Scalar" = lib.no_default, - ): + self, dtype=None, copy: bool = False, na_value: Scalar = lib.no_default, + ) -> np.ndarray: """ Convert to a NumPy Array. @@ -159,7 +168,7 @@ def _hasna(self) -> bool: # source code using it.. return self._mask.any() - def isna(self): + def isna(self) -> np.ndarray: return self._mask @property @@ -167,16 +176,21 @@ def _na_value(self): return self.dtype.na_value @property - def nbytes(self): + def nbytes(self) -> int: return self._data.nbytes + self._mask.nbytes @classmethod - def _concat_same_type(cls, to_concat): + def _concat_same_type(cls: Type[BaseMaskedArrayT], to_concat) -> BaseMaskedArrayT: data = np.concatenate([x._data for x in to_concat]) mask = np.concatenate([x._mask for x in to_concat]) return cls(data, mask) - def take(self, indexer, allow_fill=False, fill_value=None): + def take( + self: BaseMaskedArrayT, + indexer, + allow_fill: bool = False, + fill_value: Optional[Scalar] = None, + ) -> BaseMaskedArrayT: # we always fill with 1 internally # to avoid upcasting data_fill_value = self._internal_fill_value if isna(fill_value) else fill_value @@ -197,13 +211,13 @@ def take(self, indexer, allow_fill=False, fill_value=None): return type(self)(result, mask, copy=False) - def copy(self): + def copy(self: BaseMaskedArrayT) -> BaseMaskedArrayT: data, mask = self._data, self._mask data = data.copy() mask = mask.copy() return type(self)(data, mask, copy=False) - def value_counts(self, dropna=True): + def value_counts(self, dropna: bool = True) -> "Series": """ Returns a Series containing counts of each unique value. diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index e573fe661106e..0e64967ce93a6 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -263,12 +263,8 @@ def __setitem__(self, key, value) -> None: value = extract_array(value, extract_numpy=True) key = check_array_indexer(self, key) - scalar_key = lib.is_scalar(key) scalar_value = lib.is_scalar(value) - if not scalar_key and scalar_value: - key = np.asarray(key) - if not scalar_value: value = np.asarray(value, dtype=self._ndarray.dtype) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 8383b783d90e7..8141e2c78a7e2 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -624,7 +624,6 @@ def _addsub_int_array( ------- result : PeriodArray """ - assert op in [operator.add, operator.sub] if op is operator.sub: other = -other diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index 8008805ddcf87..542cfd334b810 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -4,7 +4,7 @@ from collections import abc import numbers import operator -from typing import Any, Callable +from typing import Any, Callable, Union import warnings import numpy as np @@ -479,7 +479,7 @@ def sp_index(self): return self._sparse_index @property - def sp_values(self): + def sp_values(self) -> np.ndarray: """ An ndarray containing the non- ``fill_value`` values. @@ -798,13 +798,13 @@ def _get_val_at(self, loc): val = com.maybe_box_datetimelike(val, self.sp_values.dtype) return val - def take(self, indices, allow_fill=False, fill_value=None): + def take(self, indices, allow_fill=False, fill_value=None) -> "SparseArray": if is_scalar(indices): raise ValueError(f"'indices' must be an array, not a scalar '{indices}'.") indices = np.asarray(indices, dtype=np.int32) if indices.size == 0: - result = [] + result = np.array([], dtype="object") kwargs = {"dtype": self.dtype} elif allow_fill: result = self._take_with_fill(indices, fill_value=fill_value) @@ -815,7 +815,7 @@ def take(self, indices, allow_fill=False, fill_value=None): return type(self)(result, fill_value=self.fill_value, kind=self.kind, **kwargs) - def _take_with_fill(self, indices, fill_value=None): + def _take_with_fill(self, indices, fill_value=None) -> np.ndarray: if fill_value is None: fill_value = self.dtype.na_value @@ -878,7 +878,7 @@ def _take_with_fill(self, indices, fill_value=None): return taken - def _take_without_fill(self, indices): + def _take_without_fill(self, indices) -> Union[np.ndarray, "SparseArray"]: to_shift = indices < 0 indices = indices.copy() @@ -1503,7 +1503,6 @@ def make_sparse(arr, kind="block", fill_value=None, dtype=None, copy=False): ------- (sparse_values, index, fill_value) : (ndarray, SparseIndex, Scalar) """ - arr = com.values_from_object(arr) if arr.ndim > 1: diff --git a/pandas/core/arrays/sparse/dtype.py b/pandas/core/arrays/sparse/dtype.py index 1ce735421e7d6..86869f50aab8e 100644 --- a/pandas/core/arrays/sparse/dtype.py +++ b/pandas/core/arrays/sparse/dtype.py @@ -336,7 +336,6 @@ def _subtype_with_str(self): Returns ------- - >>> SparseDtype(int, 1)._subtype_with_str dtype('int64') diff --git a/pandas/core/arrays/sparse/scipy_sparse.py b/pandas/core/arrays/sparse/scipy_sparse.py index 17a953fce9ec0..e77256a5aaadd 100644 --- a/pandas/core/arrays/sparse/scipy_sparse.py +++ b/pandas/core/arrays/sparse/scipy_sparse.py @@ -17,9 +17,11 @@ def _check_is_partition(parts, whole): def _to_ijv(ss, row_levels=(0,), column_levels=(1,), sort_labels=False): - """ For arbitrary (MultiIndexed) sparse Series return + """ + For arbitrary (MultiIndexed) sparse Series return (v, i, j, ilabels, jlabels) where (v, (i, j)) is suitable for - passing to scipy.sparse.coo constructor. """ + passing to scipy.sparse.coo constructor. + """ # index and column levels must be a partition of the index _check_is_partition([row_levels, column_levels], range(ss.index.nlevels)) @@ -30,7 +32,6 @@ def _to_ijv(ss, row_levels=(0,), column_levels=(1,), sort_labels=False): def get_indexers(levels): """ Return sparse coords and dense labels for subset levels """ - # TODO: how to do this better? cleanly slice nonnull_labels given the # coord values_ilabels = [tuple(x[i] for i in levels) for x in nonnull_labels.index] @@ -44,7 +45,8 @@ def get_indexers(levels): # labels_to_i[:] = np.arange(labels_to_i.shape[0]) def _get_label_to_i_dict(labels, sort_labels=False): - """ Return dict of unique labels to number. + """ + Return dict of unique labels to number. Optionally sort by label. """ labels = Index(map(tuple, labels)).unique().tolist() # squish @@ -89,7 +91,6 @@ def _sparse_series_to_coo(ss, row_levels=(0,), column_levels=(1,), sort_labels=F levels row_levels, column_levels as the row and column labels respectively. Returns the sparse_matrix, row and column labels. """ - import scipy.sparse if ss.index.nlevels < 2: diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index d77a37ad355a7..a7b16fd86468e 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -195,9 +195,12 @@ def __init__(self, values, dtype=_TD_DTYPE, freq=None, copy=False): def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE): assert dtype == _TD_DTYPE, dtype assert isinstance(values, np.ndarray), type(values) + if values.dtype != _TD_DTYPE: + assert values.dtype == "i8" + values = values.view(_TD_DTYPE) result = object.__new__(cls) - result._data = values.view(_TD_DTYPE) + result._data = values result._freq = to_offset(freq) result._dtype = _TD_DTYPE return result diff --git a/pandas/core/base.py b/pandas/core/base.py index f3c8b50e774af..b9aeb32eea5c1 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -13,7 +13,7 @@ from pandas.compat import PYPY from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError -from pandas.util._decorators import Appender, Substitution, cache_readonly +from pandas.util._decorators import Appender, Substitution, cache_readonly, doc from pandas.util._validators import validate_bool_kwarg from pandas.core.dtypes.cast import is_nested_object @@ -927,22 +927,50 @@ def max(self, axis=None, skipna=True, *args, **kwargs): def argmax(self, axis=None, skipna=True, *args, **kwargs): """ - Return an ndarray of the maximum argument indexer. + Return int position of the largest value in the Series. + + If the maximum is achieved in multiple locations, + the first row position is returned. Parameters ---------- axis : {None} Dummy argument for consistency with Series. skipna : bool, default True + Exclude NA/null values when showing the result. + *args, **kwargs + Additional arguments and keywords for compatibility with NumPy. Returns ------- - numpy.ndarray - Indices of the maximum values. + int + Row position of the maximum values. See Also -------- - numpy.ndarray.argmax + numpy.ndarray.argmax : Equivalent method for numpy arrays. + Series.argmin : Similar method, but returning the minimum. + Series.idxmax : Return index label of the maximum values. + Series.idxmin : Return index label of the minimum values. + + Examples + -------- + Consider dataset containing cereal calories + + >>> s = pd.Series({'Corn Flakes': 100.0, 'Almond Delight': 110.0, + ... 'Cinnamon Toast Crunch': 120.0, 'Cocoa Puff': 110.0}) + >>> s + Corn Flakes 100.0 + Almond Delight 110.0 + Cinnamon Toast Crunch 120.0 + Cocoa Puff 110.0 + dtype: float64 + + >>> s.argmax() + 2 + + The maximum cereal calories is in the third element, + since series is zero-indexed. """ nv.validate_minmax_axis(axis) nv.validate_argmax_with_skipna(skipna, args, kwargs) @@ -1196,6 +1224,7 @@ def value_counts( -------- Series.count: Number of non-NA elements in a Series. DataFrame.count: Number of non-NA elements in a DataFrame. + DataFrame.value_counts: Equivalent method on DataFrames. Examples -------- @@ -1386,7 +1415,8 @@ def memory_usage(self, deep=False): v += lib.memory_usage_of_objects(self.array) return v - @Substitution( + @doc( + algorithms.factorize, values="", order="", size_hint="", @@ -1398,7 +1428,6 @@ def memory_usage(self, deep=False): """ ), ) - @Appender(algorithms._shared_docs["factorize"]) def factorize(self, sort=False, na_sentinel=-1): return algorithms.factorize(self, sort=sort, na_sentinel=na_sentinel) diff --git a/pandas/core/common.py b/pandas/core/common.py index 00c7a41477017..705c618fc49dc 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -118,7 +118,6 @@ def is_bool_indexer(key: Any) -> bool: check_array_indexer : Check that `key` is a valid array to index, and convert to an ndarray. """ - na_msg = "cannot mask with array containing NA / NaN values" if isinstance(key, (ABCSeries, np.ndarray, ABCIndex)) or ( is_array_like(key) and is_extension_array_dtype(key.dtype) ): @@ -126,16 +125,12 @@ def is_bool_indexer(key: Any) -> bool: key = np.asarray(values_from_object(key)) if not lib.is_bool_array(key): + na_msg = "Cannot mask with non-boolean array containing NA / NaN values" if isna(key).any(): raise ValueError(na_msg) return False return True 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: @@ -337,7 +332,6 @@ def apply_if_callable(maybe_callable, obj, **kwargs): obj : NDFrame **kwargs """ - if callable(maybe_callable): return maybe_callable(obj, **kwargs) @@ -412,7 +406,6 @@ def random_state(state=None): ------- np.random.RandomState """ - if is_integer(state): return np.random.RandomState(state) elif isinstance(state, np.random.RandomState): diff --git a/pandas/core/computation/eval.py b/pandas/core/computation/eval.py index 4cdf4bac61316..f6947d5ec6233 100644 --- a/pandas/core/computation/eval.py +++ b/pandas/core/computation/eval.py @@ -276,6 +276,21 @@ def eval( See the :ref:`enhancing performance ` documentation for more details. + + Examples + -------- + >>> df = pd.DataFrame({"animal": ["dog", "pig"], "age": [10, 20]}) + >>> df + animal age + 0 dog 10 + 1 pig 20 + + We can add a new column using ``pd.eval``: + + >>> pd.eval("double_age = df.age * 2", target=df) + animal age double_age + 0 dog 10 20 + 1 pig 20 40 """ inplace = validate_bool_kwarg(inplace, "inplace") diff --git a/pandas/core/computation/expr.py b/pandas/core/computation/expr.py index c26208d3b4465..c59952bea8dc0 100644 --- a/pandas/core/computation/expr.py +++ b/pandas/core/computation/expr.py @@ -599,7 +599,6 @@ def visit_Assign(self, node, **kwargs): might or might not exist in the resolvers """ - if len(node.targets) != 1: raise SyntaxError("can only assign a single expression") if not isinstance(node.targets[0], ast.Name): diff --git a/pandas/core/computation/ops.py b/pandas/core/computation/ops.py index 5563d3ae27118..7ed089b283903 100644 --- a/pandas/core/computation/ops.py +++ b/pandas/core/computation/ops.py @@ -1,4 +1,5 @@ -"""Operator classes for eval. +""" +Operator classes for eval. """ from datetime import datetime @@ -248,7 +249,8 @@ def is_datetime(self) -> bool: def _in(x, y): - """Compute the vectorized membership of ``x in y`` if possible, otherwise + """ + Compute the vectorized membership of ``x in y`` if possible, otherwise use Python. """ try: @@ -263,7 +265,8 @@ def _in(x, y): def _not_in(x, y): - """Compute the vectorized membership of ``x not in y`` if possible, + """ + Compute the vectorized membership of ``x not in y`` if possible, otherwise use Python. """ try: @@ -445,7 +448,8 @@ def evaluate(self, env, engine: str, parser, term_type, eval_in_python): return term_type(name, env=env) def convert_values(self): - """Convert datetimes to a comparable value in an expression. + """ + Convert datetimes to a comparable value in an expression. """ def stringify(value): diff --git a/pandas/core/computation/parsing.py b/pandas/core/computation/parsing.py index ce213c8532834..92a2c20cd2a9e 100644 --- a/pandas/core/computation/parsing.py +++ b/pandas/core/computation/parsing.py @@ -1,4 +1,5 @@ -""":func:`~pandas.eval` source string parsing functions +""" +:func:`~pandas.eval` source string parsing functions """ from io import StringIO diff --git a/pandas/core/computation/pytables.py b/pandas/core/computation/pytables.py index be652ca0e6a36..828ec11c2bd38 100644 --- a/pandas/core/computation/pytables.py +++ b/pandas/core/computation/pytables.py @@ -95,7 +95,6 @@ def _disallow_scalar_only_bool_ops(self): def prune(self, klass): def pr(left, right): """ create and return a new specialized BinOp from myself """ - if left is None: return right elif right is None: @@ -150,8 +149,10 @@ def is_valid(self) -> bool: @property def is_in_table(self) -> bool: - """ return True if this is a valid column name for generation (e.g. an - actual column in the table) """ + """ + return True if this is a valid column name for generation (e.g. an + actual column in the table) + """ return self.queryables.get(self.lhs) is not None @property @@ -175,8 +176,10 @@ def generate(self, v) -> str: return f"({self.lhs} {self.op} {val})" def convert_value(self, v) -> "TermValue": - """ convert the expression that is in the term to something that is - accepted by pytables """ + """ + convert the expression that is in the term to something that is + accepted by pytables + """ def stringify(value): if self.encoding is not None: @@ -474,7 +477,6 @@ def _validate_where(w): ------ TypeError : An invalid data type was passed in for w (e.g. dict). """ - if not (isinstance(w, (PyTablesExpr, str)) or is_list_like(w)): raise TypeError( "where must be passed as a string, PyTablesExpr, " @@ -501,7 +503,6 @@ class PyTablesExpr(expr.Expr): Examples -------- - 'index>=date' "columns=['A', 'D']" 'columns=A' @@ -572,7 +573,6 @@ def __repr__(self) -> str: def evaluate(self): """ create and return the numexpr condition and filter """ - try: self.condition = self.terms.prune(ConditionBinOp) except AttributeError: @@ -601,8 +601,7 @@ def __init__(self, value, converted, kind: str): self.kind = kind def tostring(self, encoding) -> str: - """ quote the string if not encoded - else encode and return """ + """ quote the string if not encoded else encode and return """ if self.kind == "string": if encoding is not None: return str(self.converted) diff --git a/pandas/core/computation/scope.py b/pandas/core/computation/scope.py index 70dcf4defdb52..937c81fdeb8d6 100644 --- a/pandas/core/computation/scope.py +++ b/pandas/core/computation/scope.py @@ -31,7 +31,8 @@ def ensure_scope( def _replacer(x) -> str: - """Replace a number with its hexadecimal representation. Used to tag + """ + Replace a number with its hexadecimal representation. Used to tag temporary variables with their calling scope's id. """ # get the hex repr of the binary char and remove 0x and pad by pad_size diff --git a/pandas/core/dtypes/base.py b/pandas/core/dtypes/base.py index eddf46ee362d6..a4f0ccc2016c0 100644 --- a/pandas/core/dtypes/base.py +++ b/pandas/core/dtypes/base.py @@ -1,5 +1,8 @@ -"""Extend pandas with custom array types""" -from typing import Any, List, Optional, Tuple, Type +""" +Extend pandas with custom array types. +""" + +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type import numpy as np @@ -7,6 +10,9 @@ from pandas.core.dtypes.generic import ABCDataFrame, ABCIndexClass, ABCSeries +if TYPE_CHECKING: + from pandas.core.arrays import ExtensionArray # noqa: F401 + class ExtensionDtype: """ @@ -26,7 +32,6 @@ class ExtensionDtype: * type * name - * construct_from_string The following attributes influence the behavior of the dtype in pandas operations @@ -71,7 +76,7 @@ class property**. class ExtensionDtype: def __from_arrow__( - self, array: pyarrow.Array/ChunkedArray + self, array: Union[pyarrow.Array, pyarrow.ChunkedArray] ) -> ExtensionArray: ... @@ -119,11 +124,11 @@ def __eq__(self, other: Any) -> bool: def __hash__(self) -> int: return hash(tuple(getattr(self, attr) for attr in self._metadata)) - def __ne__(self, other) -> bool: + def __ne__(self, other: Any) -> bool: return not self.__eq__(other) @property - def na_value(self): + def na_value(self) -> object: """ Default NA value to use for this type. @@ -181,7 +186,7 @@ def names(self) -> Optional[List[str]]: return None @classmethod - def construct_array_type(cls): + def construct_array_type(cls) -> Type["ExtensionArray"]: """ Return the array type associated with this dtype. @@ -231,8 +236,9 @@ def construct_from_string(cls, string: str): ... if match: ... return cls(**match.groupdict()) ... else: - ... raise TypeError(f"Cannot construct a '{cls.__name__}' from - ... " "'{string}'") + ... raise TypeError( + ... f"Cannot construct a '{cls.__name__}' from '{string}'" + ... ) """ if not isinstance(string, str): raise TypeError( @@ -246,7 +252,7 @@ def construct_from_string(cls, string: str): return cls() @classmethod - def is_dtype(cls, dtype) -> bool: + def is_dtype(cls, dtype: object) -> bool: """ Check if we match 'dtype'. @@ -257,7 +263,7 @@ def is_dtype(cls, dtype) -> bool: Returns ------- - is_dtype : bool + bool Notes ----- diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 0719b8ce6010b..c2b600b5d8c5b 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -1,4 +1,6 @@ -""" routings for casting """ +""" +Routines for casting. +""" from datetime import date, datetime, timedelta @@ -76,7 +78,6 @@ def maybe_convert_platform(values): """ try to do platform conversion, allow ndarray or list here """ - if isinstance(values, (list, tuple, range)): values = construct_1d_object_array_from_listlike(values) if getattr(values, "dtype", None) == np.object_: @@ -95,7 +96,6 @@ def is_nested_object(obj) -> bool: This may not be necessarily be performant. """ - if isinstance(obj, ABCSeries) and is_object_dtype(obj): if any(isinstance(v, ABCSeries) for v in obj.values): @@ -105,7 +105,8 @@ def is_nested_object(obj) -> bool: def maybe_downcast_to_dtype(result, dtype): - """ try to cast to the specified dtype (e.g. convert back to bool/int + """ + try to cast to the specified dtype (e.g. convert back to bool/int or could be an astype of float64->float32 """ do_round = False @@ -269,12 +270,12 @@ def maybe_upcast_putmask(result: np.ndarray, mask: np.ndarray, other): Examples -------- - >>> result, _ = maybe_upcast_putmask(np.arange(1,6), - np.array([False, True, False, True, True]), np.arange(21,23)) + >>> arr = np.arange(1, 6) + >>> mask = np.array([False, True, False, True, True]) + >>> result, _ = maybe_upcast_putmask(arr, mask, False) >>> result - array([1, 21, 3, 22, 21]) + array([1, 0, 3, 0, 0]) """ - if not isinstance(result, np.ndarray): raise ValueError("The result input must be a ndarray.") if not is_scalar(other): @@ -523,7 +524,6 @@ def _ensure_dtype_type(value, dtype): ------- object """ - # Start with exceptions in which we do _not_ cast to numpy types if is_extension_array_dtype(dtype): return value @@ -564,7 +564,6 @@ def infer_dtype_from_scalar(val, pandas_dtype: bool = False): If False, scalar belongs to pandas extension types is inferred as object """ - dtype = np.object_ # a 1-element ndarray @@ -662,9 +661,8 @@ def infer_dtype_from_array(arr, pandas_dtype: bool = False): array(['1', '1'], dtype='>> infer_dtype_from_array([1, '1']) - (numpy.object_, [1, '1']) + (, [1, '1']) """ - if isinstance(arr, np.ndarray): return arr.dtype, arr @@ -709,7 +707,7 @@ def maybe_infer_dtype_type(element): >>> from collections import namedtuple >>> Foo = namedtuple("Foo", "dtype") >>> maybe_infer_dtype_type(Foo(np.dtype("i8"))) - numpy.int64 + dtype('int64') """ tipo = None if hasattr(element, "dtype"): @@ -753,7 +751,8 @@ def maybe_upcast(values, fill_value=np.nan, dtype=None, copy: bool = False): def invalidate_string_dtypes(dtype_set): - """Change string like dtypes to object for + """ + Change string like dtypes to object for ``DataFrame.select_dtypes()``. """ non_string_dtypes = dtype_set - {np.dtype("S").type, np.dtype(">> Series([-1], dtype="uint64") + >>> pd.Series([-1], dtype="uint64") Traceback (most recent call last): ... OverflowError: Trying to coerce negative values to unsigned integers Also, if you try to coerce float values to integers, it raises: - >>> Series([1, 2, 3.5], dtype="int64") + >>> pd.Series([1, 2, 3.5], dtype="int64") Traceback (most recent call last): ... ValueError: Trying to coerce float values to integers """ - try: if not hasattr(arr, "astype"): casted = np.array(arr, dtype=dtype, copy=copy) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index eb9b880cd10d9..c0420244f671e 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1,4 +1,7 @@ -""" common type operations """ +""" +Common type operations. +""" + from typing import Any, Callable, Union import warnings @@ -89,7 +92,6 @@ def ensure_float(arr): float_arr : The original array cast to the float dtype if possible. Otherwise, the original array is returned. """ - if issubclass(arr.dtype.type, (np.integer, np.bool_)): arr = arr.astype(float) return arr @@ -129,7 +131,6 @@ def ensure_categorical(arr): cat_arr : The original array cast as a Categorical. If it already is a Categorical, we return as is. """ - if not is_categorical(arr): from pandas import Categorical @@ -322,7 +323,6 @@ def is_scipy_sparse(arr) -> bool: >>> is_scipy_sparse(pd.arrays.SparseArray([1, 2, 3])) False """ - global _is_scipy_sparse if _is_scipy_sparse is None: @@ -364,7 +364,6 @@ def is_categorical(arr) -> bool: >>> is_categorical(pd.CategoricalIndex([1, 2, 3])) True """ - return isinstance(arr, ABCCategorical) or is_categorical_dtype(arr) @@ -395,7 +394,6 @@ def is_datetime64_dtype(arr_or_dtype) -> bool: >>> is_datetime64_dtype([1, 2, 3]) False """ - return _is_dtype_type(arr_or_dtype, classes(np.datetime64)) @@ -431,7 +429,6 @@ def is_datetime64tz_dtype(arr_or_dtype) -> bool: >>> is_datetime64tz_dtype(s) True """ - if arr_or_dtype is None: return False return DatetimeTZDtype.is_dtype(arr_or_dtype) @@ -464,7 +461,6 @@ def is_timedelta64_dtype(arr_or_dtype) -> bool: >>> is_timedelta64_dtype('0 days') False """ - return _is_dtype_type(arr_or_dtype, classes(np.timedelta64)) @@ -495,7 +491,6 @@ def is_period_dtype(arr_or_dtype) -> bool: >>> is_period_dtype(pd.PeriodIndex([], freq="A")) True """ - # TODO: Consider making Period an instance of PeriodDtype if arr_or_dtype is None: return False @@ -531,7 +526,6 @@ def is_interval_dtype(arr_or_dtype) -> bool: >>> is_interval_dtype(pd.IntervalIndex([interval])) True """ - # TODO: Consider making Interval an instance of IntervalDtype if arr_or_dtype is None: return False @@ -565,7 +559,6 @@ def is_categorical_dtype(arr_or_dtype) -> bool: >>> is_categorical_dtype(pd.CategoricalIndex([1, 2, 3])) True """ - if arr_or_dtype is None: return False return CategoricalDtype.is_dtype(arr_or_dtype) @@ -599,7 +592,6 @@ def is_string_dtype(arr_or_dtype) -> bool: >>> is_string_dtype(pd.Series([1, 2])) False """ - # TODO: gh-15585: consider making the checks stricter. def condition(dtype) -> bool: return dtype.kind in ("O", "S", "U") and not is_excluded_dtype(dtype) @@ -638,7 +630,6 @@ def is_period_arraylike(arr) -> bool: >>> is_period_arraylike(pd.PeriodIndex(["2017-01-01"], freq="D")) True """ - if isinstance(arr, (ABCPeriodIndex, ABCPeriodArray)): return True elif isinstance(arr, (np.ndarray, ABCSeries)): @@ -670,7 +661,6 @@ def is_datetime_arraylike(arr) -> bool: >>> is_datetime_arraylike(pd.DatetimeIndex([1, 2, 3])) True """ - if isinstance(arr, ABCDatetimeIndex): return True elif isinstance(arr, (np.ndarray, ABCSeries)): @@ -705,10 +695,9 @@ def is_dtype_equal(source, target) -> bool: False >>> is_dtype_equal(CategoricalDtype(), "category") True - >>> is_dtype_equal(DatetimeTZDtype(), "datetime64") + >>> is_dtype_equal(DatetimeTZDtype(tz="UTC"), "datetime64") False """ - try: source = _get_dtype(source) target = _get_dtype(target) @@ -767,7 +756,6 @@ def is_any_int_dtype(arr_or_dtype) -> bool: >>> is_any_int_dtype(pd.Index([1, 2.])) # float False """ - return _is_dtype_type(arr_or_dtype, classes(np.integer, np.timedelta64)) @@ -822,7 +810,6 @@ def is_integer_dtype(arr_or_dtype) -> bool: >>> is_integer_dtype(pd.Index([1, 2.])) # float False """ - return _is_dtype_type(arr_or_dtype, classes_and_not_datetimelike(np.integer)) @@ -862,7 +849,7 @@ def is_signed_integer_dtype(arr_or_dtype) -> bool: True >>> is_signed_integer_dtype('Int8') True - >>> is_signed_dtype(pd.Int8Dtype) + >>> is_signed_integer_dtype(pd.Int8Dtype) True >>> is_signed_integer_dtype(np.datetime64) False @@ -879,7 +866,6 @@ def is_signed_integer_dtype(arr_or_dtype) -> bool: >>> is_signed_integer_dtype(np.array([1, 2], dtype=np.uint32)) # unsigned False """ - return _is_dtype_type(arr_or_dtype, classes_and_not_datetimelike(np.signedinteger)) @@ -979,7 +965,6 @@ def is_int64_dtype(arr_or_dtype) -> bool: >>> is_int64_dtype(np.array([1, 2], dtype=np.uint32)) # unsigned False """ - return _is_dtype_type(arr_or_dtype, classes(np.int64)) @@ -994,7 +979,7 @@ def is_datetime64_any_dtype(arr_or_dtype) -> bool: Returns ------- - boolean + bool Whether or not the array or dtype is of the datetime64 dtype. Examples @@ -1011,13 +996,11 @@ def is_datetime64_any_dtype(arr_or_dtype) -> bool: False >>> is_datetime64_any_dtype(np.array([1, 2])) False - >>> is_datetime64_any_dtype(np.array([], dtype=np.datetime64)) + >>> is_datetime64_any_dtype(np.array([], dtype="datetime64[ns]")) True - >>> is_datetime64_any_dtype(pd.DatetimeIndex([1, 2, 3], - dtype=np.datetime64)) + >>> is_datetime64_any_dtype(pd.DatetimeIndex([1, 2, 3], dtype="datetime64[ns]")) True """ - if arr_or_dtype is None: return False return is_datetime64_dtype(arr_or_dtype) or is_datetime64tz_dtype(arr_or_dtype) @@ -1034,7 +1017,7 @@ def is_datetime64_ns_dtype(arr_or_dtype) -> bool: Returns ------- - boolean + bool Whether or not the array or dtype is of the datetime64[ns] dtype. Examples @@ -1051,16 +1034,13 @@ def is_datetime64_ns_dtype(arr_or_dtype) -> bool: False >>> is_datetime64_ns_dtype(np.array([1, 2])) False - >>> is_datetime64_ns_dtype(np.array([], dtype=np.datetime64)) # no unit + >>> is_datetime64_ns_dtype(np.array([], dtype="datetime64")) # no unit False - >>> is_datetime64_ns_dtype(np.array([], - dtype="datetime64[ps]")) # wrong unit + >>> is_datetime64_ns_dtype(np.array([], dtype="datetime64[ps]")) # wrong unit False - >>> is_datetime64_ns_dtype(pd.DatetimeIndex([1, 2, 3], - dtype=np.datetime64)) # has 'ns' unit + >>> is_datetime64_ns_dtype(pd.DatetimeIndex([1, 2, 3], dtype="datetime64[ns]")) True """ - if arr_or_dtype is None: return False try: @@ -1139,7 +1119,6 @@ def is_datetime_or_timedelta_dtype(arr_or_dtype) -> bool: >>> is_datetime_or_timedelta_dtype(np.array([], dtype=np.datetime64)) True """ - return _is_dtype_type(arr_or_dtype, classes(np.datetime64, np.timedelta64)) @@ -1200,7 +1179,6 @@ def is_numeric_v_string_like(a, b): >>> is_numeric_v_string_like(np.array(["foo"]), np.array(["foo"])) False """ - is_a_array = isinstance(a, np.ndarray) is_b_array = isinstance(b, np.ndarray) @@ -1240,7 +1218,8 @@ def is_datetimelike_v_numeric(a, b): Examples -------- - >>> dt = np.datetime64(pd.datetime(2017, 1, 1)) + >>> from datetime import datetime + >>> dt = np.datetime64(datetime(2017, 1, 1)) >>> >>> is_datetimelike_v_numeric(1, 1) False @@ -1261,7 +1240,6 @@ def is_datetimelike_v_numeric(a, b): >>> is_datetimelike_v_numeric(np.array([dt]), np.array([dt])) False """ - if not hasattr(a, "dtype"): a = np.asarray(a) if not hasattr(b, "dtype"): @@ -1312,7 +1290,6 @@ def needs_i8_conversion(arr_or_dtype) -> bool: >>> needs_i8_conversion(pd.DatetimeIndex([1, 2, 3], tz="US/Eastern")) True """ - if arr_or_dtype is None: return False return ( @@ -1359,7 +1336,6 @@ def is_numeric_dtype(arr_or_dtype) -> bool: >>> is_numeric_dtype(np.array([], dtype=np.timedelta64)) False """ - return _is_dtype_type( arr_or_dtype, classes_and_not_datetimelike(np.number, np.bool_) ) @@ -1393,7 +1369,6 @@ def is_string_like_dtype(arr_or_dtype) -> bool: >>> is_string_like_dtype(pd.Series([1, 2])) False """ - return _is_dtype(arr_or_dtype, lambda dtype: dtype.kind in ("S", "U")) @@ -1639,7 +1614,6 @@ def is_complex_dtype(arr_or_dtype) -> bool: >>> is_complex_dtype(np.array([1 + 1j, 5])) True """ - return _is_dtype_type(arr_or_dtype, classes(np.complexfloating)) @@ -1658,7 +1632,6 @@ def _is_dtype(arr_or_dtype, condition) -> bool: bool """ - if arr_or_dtype is None: return False try: @@ -1687,7 +1660,6 @@ def _get_dtype(arr_or_dtype) -> DtypeObj: ------ TypeError : The passed in object is None. """ - if arr_or_dtype is None: raise TypeError("Cannot deduce dtype from null object") @@ -1718,7 +1690,6 @@ def _is_dtype_type(arr_or_dtype, condition) -> bool: ------- bool : if the condition is satisfied for the arr_or_dtype """ - if arr_or_dtype is None: return condition(type(None)) @@ -1768,7 +1739,6 @@ def infer_dtype_from_object(dtype): ------- dtype_object : The extracted numpy dtype.type-style object. """ - if isinstance(dtype, type) and issubclass(dtype, np.generic): # Type object from a dtype return dtype @@ -1828,7 +1798,6 @@ def _validate_date_like_dtype(dtype) -> None: ValueError : The dtype is an illegal date-like dtype (e.g. the the frequency provided is too specific) """ - try: typ = np.datetime_data(dtype)[0] except ValueError as e: diff --git a/pandas/core/dtypes/concat.py b/pandas/core/dtypes/concat.py index cd4b5af4588e5..49034616b374a 100644 --- a/pandas/core/dtypes/concat.py +++ b/pandas/core/dtypes/concat.py @@ -1,5 +1,5 @@ """ -Utility functions related to concat +Utility functions related to concat. """ import numpy as np @@ -38,7 +38,6 @@ def get_dtype_kinds(l): ------- a set of kinds that exist in this list of arrays """ - typs = set() for arr in l: @@ -85,7 +84,6 @@ def concat_compat(to_concat, axis: int = 0): ------- a single array, preserving the combined dtypes """ - # filter empty arrays # 1-d dtypes always are included here def is_nonempty(x) -> bool: @@ -138,7 +136,8 @@ def is_nonempty(x) -> bool: def concat_categorical(to_concat, axis: int = 0): - """Concatenate an object/categorical array of arrays, each of which is a + """ + Concatenate an object/categorical array of arrays, each of which is a single dtype Parameters @@ -153,7 +152,6 @@ def concat_categorical(to_concat, axis: int = 0): Categorical A single array, preserving the combined dtypes """ - # we could have object blocks and categoricals here # if we only have a single categoricals then combine everything # else its a non-compat categorical @@ -218,13 +216,11 @@ def union_categoricals( Notes ----- - To learn more about categories, see `link `__ Examples -------- - >>> from pandas.api.types import union_categoricals If you want to combine categoricals that do not necessarily have @@ -261,6 +257,8 @@ def union_categoricals( >>> a = pd.Categorical(["a", "b"], ordered=True) >>> b = pd.Categorical(["a", "b", "c"], ordered=True) >>> union_categoricals([a, b]) + Traceback (most recent call last): + ... TypeError: to union ordered Categoricals, all categories must be the same New in version 0.20.0 @@ -379,7 +377,6 @@ def concat_datetime(to_concat, axis=0, typs=None): ------- a single array, preserving the combined dtypes """ - if typs is None: typs = get_dtype_kinds(to_concat) @@ -464,7 +461,6 @@ def _concat_sparse(to_concat, axis=0, typs=None): ------- a single array, preserving the combined dtypes """ - from pandas.core.arrays import SparseArray fill_values = [x.fill_value for x in to_concat if isinstance(x, SparseArray)] diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index d00b46700981c..0730de934b56c 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1,4 +1,7 @@ -""" define extension dtypes """ +""" +Define extension dtypes. +""" + import re from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Type, Union, cast @@ -286,23 +289,27 @@ def _from_values_or_dtype( Examples -------- - >>> CategoricalDtype._from_values_or_dtype() + >>> pd.CategoricalDtype._from_values_or_dtype() CategoricalDtype(categories=None, ordered=None) - >>> CategoricalDtype._from_values_or_dtype(categories=['a', 'b'], - ... ordered=True) + >>> pd.CategoricalDtype._from_values_or_dtype( + ... categories=['a', 'b'], ordered=True + ... ) CategoricalDtype(categories=['a', 'b'], ordered=True) - >>> dtype1 = CategoricalDtype(['a', 'b'], ordered=True) - >>> dtype2 = CategoricalDtype(['x', 'y'], ordered=False) - >>> c = Categorical([0, 1], dtype=dtype1, fastpath=True) - >>> CategoricalDtype._from_values_or_dtype(c, ['x', 'y'], ordered=True, - ... dtype=dtype2) + >>> dtype1 = pd.CategoricalDtype(['a', 'b'], ordered=True) + >>> dtype2 = pd.CategoricalDtype(['x', 'y'], ordered=False) + >>> c = pd.Categorical([0, 1], dtype=dtype1, fastpath=True) + >>> pd.CategoricalDtype._from_values_or_dtype( + ... c, ['x', 'y'], ordered=True, dtype=dtype2 + ... ) + Traceback (most recent call last): + ... ValueError: Cannot specify `categories` or `ordered` together with `dtype`. The supplied dtype takes precedence over values' dtype: - >>> CategoricalDtype._from_values_or_dtype(c, dtype=dtype2) - CategoricalDtype(['x', 'y'], ordered=False) + >>> pd.CategoricalDtype._from_values_or_dtype(c, dtype=dtype2) + CategoricalDtype(categories=['x', 'y'], ordered=False) """ from pandas.core.dtypes.common import is_categorical @@ -317,6 +324,8 @@ def _from_values_or_dtype( raise ValueError( "Cannot specify `categories` or `ordered` together with `dtype`." ) + elif not isinstance(dtype, CategoricalDtype): + raise ValueError(f"Cannot not construct CategoricalDtype from {dtype}") elif is_categorical(values): # If no "dtype" was passed, use the one from "values", but honor # the "ordered" and "categories" arguments @@ -824,7 +833,6 @@ def __new__(cls, freq=None): ---------- freq : frequency """ - if isinstance(freq, PeriodDtype): return freq @@ -923,7 +931,6 @@ def is_dtype(cls, dtype) -> bool: Return a boolean if we if the passed type is an actual dtype that we can match (via string or type) """ - if isinstance(dtype, str): # PeriodDtype can be instantiated from freq string like "U", # but doesn't regard freq str like "U" as dtype. @@ -1132,7 +1139,6 @@ def is_dtype(cls, dtype) -> bool: Return a boolean if we if the passed type is an actual dtype that we can match (via string or type) """ - if isinstance(dtype, str): if dtype.lower().startswith("interval"): try: diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index 37bca76802843..56b880dca1241 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -65,7 +65,6 @@ def is_number(obj) -> bool: >>> pd.api.types.is_number("5") False """ - return isinstance(obj, (Number, np.number)) @@ -91,7 +90,6 @@ def _iterable_not_string(obj) -> bool: >>> _iterable_not_string(1) False """ - return isinstance(obj, abc.Iterable) and not isinstance(obj, str) @@ -117,13 +115,13 @@ def is_file_like(obj) -> bool: Examples -------- - >>> buffer(StringIO("data")) + >>> import io + >>> buffer = io.StringIO("data") >>> is_file_like(buffer) True >>> is_file_like([1, 2, 3]) False """ - if not (hasattr(obj, "read") or hasattr(obj, "write")): return False @@ -176,7 +174,6 @@ def is_re_compilable(obj) -> bool: >>> is_re_compilable(1) False """ - try: re.compile(obj) except TypeError: @@ -214,7 +211,6 @@ def is_array_like(obj) -> bool: >>> is_array_like(("a", "b")) False """ - return is_list_like(obj) and hasattr(obj, "dtype") @@ -311,6 +307,7 @@ def is_named_tuple(obj) -> bool: Examples -------- + >>> from collections import namedtuple >>> Point = namedtuple("Point", ["x", "y"]) >>> p = Point(1, 2) >>> @@ -319,7 +316,6 @@ def is_named_tuple(obj) -> bool: >>> is_named_tuple((1, 2)) False """ - return isinstance(obj, tuple) and hasattr(obj, "_fields") @@ -339,6 +335,7 @@ def is_hashable(obj) -> bool: Examples -------- + >>> import collections >>> a = ([],) >>> isinstance(a, collections.abc.Hashable) True @@ -383,7 +380,6 @@ def is_sequence(obj) -> bool: >>> is_sequence(iter(l)) False """ - try: iter(obj) # Can iterate over it. len(obj) # Has a length associated with it. diff --git a/pandas/core/dtypes/missing.py b/pandas/core/dtypes/missing.py index 0bc754b3e8fb3..ee74b02af9516 100644 --- a/pandas/core/dtypes/missing.py +++ b/pandas/core/dtypes/missing.py @@ -430,7 +430,6 @@ def array_equivalent(left, right, strict_nan: bool = False) -> bool: ... np.array([1, 2, np.nan])) False """ - left, right = np.asarray(left), np.asarray(right) # shape compat @@ -504,7 +503,6 @@ def _infer_fill_value(val): scalar/ndarray/list-like if we are a NaT, return the correct dtyped element to provide proper block construction """ - if not is_list_like(val): val = [val] val = np.array(val, copy=False) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index e0efa93379bca..6f5aef4884ccd 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -14,7 +14,6 @@ import datetime from io import StringIO import itertools -import sys from textwrap import dedent from typing import ( IO, @@ -49,6 +48,7 @@ Appender, Substitution, deprecate_kwarg, + doc, rewrite_axis_style_signature, ) from pandas.util._validators import ( @@ -111,7 +111,7 @@ from pandas.core.indexes import base as ibase from pandas.core.indexes.api import Index, ensure_index, ensure_index_from_sequences from pandas.core.indexes.datetimes import DatetimeIndex -from pandas.core.indexes.multi import maybe_droplevels +from pandas.core.indexes.multi import MultiIndex, maybe_droplevels from pandas.core.indexes.period import PeriodIndex from pandas.core.indexing import check_bool_indexer, convert_to_index_sliceable from pandas.core.internals import BlockManager @@ -130,7 +130,7 @@ from pandas.io.common import get_filepath_or_buffer from pandas.io.formats import console, format as fmt -from pandas.io.formats.printing import pprint_thing +from pandas.io.formats.info import info import pandas.plotting if TYPE_CHECKING: @@ -514,7 +514,7 @@ def __init__( else: raise ValueError("DataFrame constructor not properly called!") - NDFrame.__init__(self, mgr, fastpath=True) + NDFrame.__init__(self, mgr) # ---------------------------------------------------------------------- @@ -832,7 +832,6 @@ def style(self) -> "Styler": Returns a Styler object. Contains methods for building a styled HTML representation of the DataFrame. - a styled HTML representation fo the DataFrame. See Also -------- @@ -926,7 +925,6 @@ def iterrows(self) -> Iterable[Tuple[Optional[Hashable], Series]]: Notes ----- - 1. Because ``iterrows`` returns a Series for each row, it does **not** preserve dtypes across the rows (dtypes are preserved across columns for DataFrames). For example, @@ -2225,282 +2223,11 @@ def to_html( ) # ---------------------------------------------------------------------- - + @Appender(info.__doc__) def info( self, verbose=None, buf=None, max_cols=None, memory_usage=None, null_counts=None ) -> None: - """ - Print a concise summary of a DataFrame. - - This method prints information about a DataFrame including - the index dtype and column dtypes, non-null values and memory usage. - - Parameters - ---------- - verbose : bool, optional - Whether to print the full summary. By default, the setting in - ``pandas.options.display.max_info_columns`` is followed. - buf : writable buffer, defaults to sys.stdout - Where to send the output. By default, the output is printed to - sys.stdout. Pass a writable buffer if you need to further process - the output. - max_cols : int, optional - When to switch from the verbose to the truncated output. If the - DataFrame has more than `max_cols` columns, the truncated output - is used. By default, the setting in - ``pandas.options.display.max_info_columns`` is used. - memory_usage : bool, str, optional - Specifies whether total memory usage of the DataFrame - elements (including the index) should be displayed. By default, - this follows the ``pandas.options.display.memory_usage`` setting. - - True always show memory usage. False never shows memory usage. - A value of 'deep' is equivalent to "True with deep introspection". - Memory usage is shown in human-readable units (base-2 - representation). Without deep introspection a memory estimation is - made based in column dtype and number of rows assuming values - consume the same memory amount for corresponding dtypes. With deep - memory introspection, a real memory usage calculation is performed - at the cost of computational resources. - null_counts : bool, optional - Whether to show the non-null counts. By default, this is shown - only if the frame is smaller than - ``pandas.options.display.max_info_rows`` and - ``pandas.options.display.max_info_columns``. A value of True always - shows the counts, and False never shows the counts. - - Returns - ------- - None - This method prints a summary of a DataFrame and returns None. - - See Also - -------- - DataFrame.describe: Generate descriptive statistics of DataFrame - columns. - DataFrame.memory_usage: Memory usage of DataFrame columns. - - Examples - -------- - >>> int_values = [1, 2, 3, 4, 5] - >>> text_values = ['alpha', 'beta', 'gamma', 'delta', 'epsilon'] - >>> float_values = [0.0, 0.25, 0.5, 0.75, 1.0] - >>> df = pd.DataFrame({"int_col": int_values, "text_col": text_values, - ... "float_col": float_values}) - >>> df - int_col text_col float_col - 0 1 alpha 0.00 - 1 2 beta 0.25 - 2 3 gamma 0.50 - 3 4 delta 0.75 - 4 5 epsilon 1.00 - - Prints information of all columns: - - >>> df.info(verbose=True) - - RangeIndex: 5 entries, 0 to 4 - Data columns (total 3 columns): - # Column Non-Null Count Dtype - --- ------ -------------- ----- - 0 int_col 5 non-null int64 - 1 text_col 5 non-null object - 2 float_col 5 non-null float64 - dtypes: float64(1), int64(1), object(1) - memory usage: 248.0+ bytes - - Prints a summary of columns count and its dtypes but not per column - information: - - >>> df.info(verbose=False) - - RangeIndex: 5 entries, 0 to 4 - Columns: 3 entries, int_col to float_col - dtypes: float64(1), int64(1), object(1) - memory usage: 248.0+ bytes - - Pipe output of DataFrame.info to buffer instead of sys.stdout, get - buffer content and writes to a text file: - - >>> import io - >>> buffer = io.StringIO() - >>> df.info(buf=buffer) - >>> s = buffer.getvalue() - >>> with open("df_info.txt", "w", - ... encoding="utf-8") as f: # doctest: +SKIP - ... f.write(s) - 260 - - The `memory_usage` parameter allows deep introspection mode, specially - useful for big DataFrames and fine-tune memory optimization: - - >>> random_strings_array = np.random.choice(['a', 'b', 'c'], 10 ** 6) - >>> df = pd.DataFrame({ - ... 'column_1': np.random.choice(['a', 'b', 'c'], 10 ** 6), - ... 'column_2': np.random.choice(['a', 'b', 'c'], 10 ** 6), - ... 'column_3': np.random.choice(['a', 'b', 'c'], 10 ** 6) - ... }) - >>> df.info() - - RangeIndex: 1000000 entries, 0 to 999999 - Data columns (total 3 columns): - # Column Non-Null Count Dtype - --- ------ -------------- ----- - 0 column_1 1000000 non-null object - 1 column_2 1000000 non-null object - 2 column_3 1000000 non-null object - dtypes: object(3) - memory usage: 22.9+ MB - - >>> df.info(memory_usage='deep') - - RangeIndex: 1000000 entries, 0 to 999999 - Data columns (total 3 columns): - # Column Non-Null Count Dtype - --- ------ -------------- ----- - 0 column_1 1000000 non-null object - 1 column_2 1000000 non-null object - 2 column_3 1000000 non-null object - dtypes: object(3) - memory usage: 188.8 MB - """ - if buf is None: # pragma: no cover - buf = sys.stdout - - lines = [] - - lines.append(str(type(self))) - lines.append(self.index._summary()) - - if len(self.columns) == 0: - lines.append(f"Empty {type(self).__name__}") - fmt.buffer_put_lines(buf, lines) - return - - cols = self.columns - col_count = len(self.columns) - - # hack - if max_cols is None: - max_cols = get_option("display.max_info_columns", len(self.columns) + 1) - - max_rows = get_option("display.max_info_rows", len(self) + 1) - - if null_counts is None: - show_counts = (col_count <= max_cols) and (len(self) < max_rows) - else: - show_counts = null_counts - exceeds_info_cols = col_count > max_cols - - def _verbose_repr(): - lines.append(f"Data columns (total {len(self.columns)} columns):") - - id_head = " # " - column_head = "Column" - col_space = 2 - - max_col = max(len(pprint_thing(k)) for k in cols) - len_column = len(pprint_thing(column_head)) - space = max(max_col, len_column) + col_space - - max_id = len(pprint_thing(col_count)) - len_id = len(pprint_thing(id_head)) - space_num = max(max_id, len_id) + col_space - counts = None - - header = _put_str(id_head, space_num) + _put_str(column_head, space) - if show_counts: - counts = self.count() - if len(cols) != len(counts): # pragma: no cover - raise AssertionError( - f"Columns must equal counts ({len(cols)} != {len(counts)})" - ) - count_header = "Non-Null Count" - len_count = len(count_header) - non_null = " non-null" - max_count = max(len(pprint_thing(k)) for k in counts) + len(non_null) - space_count = max(len_count, max_count) + col_space - count_temp = "{count}" + non_null - else: - count_header = "" - space_count = len(count_header) - len_count = space_count - count_temp = "{count}" - - dtype_header = "Dtype" - len_dtype = len(dtype_header) - max_dtypes = max(len(pprint_thing(k)) for k in self.dtypes) - space_dtype = max(len_dtype, max_dtypes) - header += _put_str(count_header, space_count) + _put_str( - dtype_header, space_dtype - ) - - lines.append(header) - lines.append( - _put_str("-" * len_id, space_num) - + _put_str("-" * len_column, space) - + _put_str("-" * len_count, space_count) - + _put_str("-" * len_dtype, space_dtype) - ) - - for i, col in enumerate(self.columns): - dtype = self.dtypes.iloc[i] - col = pprint_thing(col) - - line_no = _put_str(f" {i}", space_num) - count = "" - if show_counts: - count = counts.iloc[i] - - lines.append( - line_no - + _put_str(col, space) - + _put_str(count_temp.format(count=count), space_count) - + _put_str(dtype, space_dtype) - ) - - def _non_verbose_repr(): - lines.append(self.columns._summary(name="Columns")) - - def _sizeof_fmt(num, size_qualifier): - # returns size in human readable format - for x in ["bytes", "KB", "MB", "GB", "TB"]: - if num < 1024.0: - return f"{num:3.1f}{size_qualifier} {x}" - num /= 1024.0 - return f"{num:3.1f}{size_qualifier} PB" - - if verbose: - _verbose_repr() - elif verbose is False: # specifically set to False, not nesc None - _non_verbose_repr() - else: - if exceeds_info_cols: - _non_verbose_repr() - else: - _verbose_repr() - - counts = self._data.get_dtype_counts() - dtypes = [f"{k[0]}({k[1]:d})" for k in sorted(counts.items())] - lines.append(f"dtypes: {', '.join(dtypes)}") - - if memory_usage is None: - memory_usage = get_option("display.memory_usage") - if memory_usage: - # append memory usage of df to display - size_qualifier = "" - if memory_usage == "deep": - deep = True - else: - # size_qualifier is just a best effort; not guaranteed to catch - # all cases (e.g., it misses categorical data even with object - # categories) - deep = False - if "object" in counts or self.index._is_memory_usage_qualified(): - size_qualifier = "+" - mem_usage = self.memory_usage(index=True, deep=deep).sum() - lines.append(f"memory usage: {_sizeof_fmt(mem_usage, size_qualifier)}\n") - fmt.buffer_put_lines(buf, lines) + return info(self, verbose, buf, max_cols, memory_usage, null_counts) def memory_usage(self, index=True, deep=False) -> Series: """ @@ -2894,8 +2621,8 @@ def _get_value(self, index, col, takeable: bool = False): scalar """ if takeable: - series = self._iget_item_cache(col) - return com.maybe_box_datetimelike(series._values[index]) + series = self._ixs(col, axis=1) + return series._values[index] series = self._get_item_cache(col) engine = self.index._engine @@ -2933,12 +2660,12 @@ def __setitem__(self, key, value): # set column self._set_item(key, value) - def _setitem_slice(self, key, value): + def _setitem_slice(self, key: slice, value): # NB: we can't just use self.loc[key] = value because that # operates on labels and we need to operate positional for # backwards-compat, xref GH#31469 self._check_setitem_copy() - self.loc._setitem_with_indexer(key, value) + self.iloc._setitem_with_indexer(key, value) def _setitem_array(self, key, value): # also raises Exception if object array with NA values @@ -2950,7 +2677,7 @@ def _setitem_array(self, key, value): key = check_bool_indexer(self.index, key) indexer = key.nonzero()[0] self._check_setitem_copy() - self.loc._setitem_with_indexer(indexer, value) + self.iloc._setitem_with_indexer(indexer, value) else: if isinstance(value, DataFrame): if len(value.columns) != len(key): @@ -2962,7 +2689,7 @@ def _setitem_array(self, key, value): key, axis=1, raise_missing=False )[1] self._check_setitem_copy() - self.loc._setitem_with_indexer((slice(None), indexer), value) + self.iloc._setitem_with_indexer((slice(None), indexer), value) def _setitem_frame(self, key, value): # support boolean setting with DataFrame input, e.g. @@ -3011,17 +2738,12 @@ def _set_value(self, index, col, value, takeable: bool = False): col : column label value : scalar takeable : interpret the index/col as indexers, default False - - Returns - ------- - DataFrame - If label pair is contained, will be reference to calling DataFrame, - otherwise a new object. """ try: if takeable is True: series = self._iget_item_cache(col) - return series._set_value(index, value, takeable=True) + series._set_value(index, value, takeable=True) + return series = self._get_item_cache(col) engine = self.index._engine @@ -3031,7 +2753,6 @@ def _set_value(self, index, col, value, takeable: bool = False): series._values[loc] = value # Note: trying to use series._set_value breaks tests in # tests.frame.indexing.test_indexing and tests.indexing.test_partial - return self except (KeyError, TypeError): # set using a non-recursive method & reset the cache if takeable: @@ -3040,8 +2761,6 @@ def _set_value(self, index, col, value, takeable: bool = False): self.loc[index, col] = value self._item_cache.pop(col, None) - return self - def _ensure_valid_index(self, value): """ Ensure that if we don't have an index, that we can create one from the @@ -3327,6 +3046,21 @@ def eval(self, expr, inplace=False, **kwargs): 2 3 6 9 3 4 4 8 4 5 2 7 + + Multiple columns can be assigned to using multi-line expressions: + + >>> df.eval( + ... ''' + ... C = A + B + ... D = A - B + ... ''' + ... ) + A B C D + 0 1 10 11 -9 + 1 2 8 10 -6 + 2 3 6 9 -3 + 3 4 4 8 0 + 4 5 2 7 3 """ from pandas.core.computation.eval import eval as _eval @@ -3828,6 +3562,8 @@ def align( @Appender( """ + Examples + -------- >>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) Change the row labels. @@ -4045,7 +3781,6 @@ def rename( level: Optional[Level] = None, errors: str = "ignore", ) -> Optional["DataFrame"]: - """ Alter axes labels. @@ -4164,8 +3899,7 @@ def rename( errors=errors, ) - @Substitution(**_shared_doc_kwargs) - @Appender(NDFrame.fillna.__doc__) + @doc(NDFrame.fillna, **_shared_doc_kwargs) def fillna( self, value=None, @@ -4834,6 +4568,10 @@ def drop_duplicates( ------- DataFrame DataFrame with duplicates removed or None if ``inplace=True``. + + See Also + -------- + DataFrame.value_counts: Count unique combinations of columns. """ if self.empty: return self.copy() @@ -4997,8 +4735,9 @@ def sort_index( and 1 identifies the columns. level : int or level name or list of ints or list of level names If not None, sort on values in specified index level(s). - ascending : bool, default True - Sort ascending vs. descending. + ascending : bool or list of bools, default True + Sort ascending vs. descending. When the index is a MultiIndex the + sort direction can be controlled for each level individually. inplace : bool, default False If True, perform operation in-place. kind : {'quicksort', 'mergesort', 'heapsort'}, default 'quicksort' @@ -5022,7 +4761,6 @@ def sort_index( sorted_obj : DataFrame or None DataFrame with sorted index if inplace=False, None otherwise. """ - # TODO: this can be combined with Series.sort_index impl as # almost identical @@ -5079,6 +4817,102 @@ def sort_index( else: return self._constructor(new_data).__finalize__(self) + def value_counts( + self, + subset: Optional[Sequence[Label]] = None, + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + ): + """ + Return a Series containing counts of unique rows in the DataFrame. + + .. versionadded:: 1.1.0 + + Parameters + ---------- + subset : list-like, optional + Columns to use when counting unique combinations. + normalize : bool, default False + Return proportions rather than frequencies. + sort : bool, default True + Sort by frequencies. + ascending : bool, default False + Sort in ascending order. + + Returns + ------- + Series + + See Also + -------- + Series.value_counts: Equivalent method on Series. + + Notes + ----- + The returned Series will have a MultiIndex with one level per input + column. By default, rows that contain any NA values are omitted from + the result. By default, the resulting Series will be in descending + order so that the first element is the most frequently-occurring row. + + Examples + -------- + >>> df = pd.DataFrame({'num_legs': [2, 4, 4, 6], + ... 'num_wings': [2, 0, 0, 0]}, + ... index=['falcon', 'dog', 'cat', 'ant']) + >>> df + num_legs num_wings + falcon 2 2 + dog 4 0 + cat 4 0 + ant 6 0 + + >>> df.value_counts() + num_legs num_wings + 4 0 2 + 6 0 1 + 2 2 1 + dtype: int64 + + >>> df.value_counts(sort=False) + num_legs num_wings + 2 2 1 + 4 0 2 + 6 0 1 + dtype: int64 + + >>> df.value_counts(ascending=True) + num_legs num_wings + 2 2 1 + 6 0 1 + 4 0 2 + dtype: int64 + + >>> df.value_counts(normalize=True) + num_legs num_wings + 4 0 0.50 + 6 0 0.25 + 2 2 0.25 + dtype: float64 + """ + if subset is None: + subset = self.columns.tolist() + + counts = self.groupby(subset).size() + + if sort: + counts = counts.sort_values(ascending=ascending) + if normalize: + counts /= counts.sum() + + # Force MultiIndex for single column + if len(subset) == 1: + counts.index = MultiIndex.from_arrays( + [counts.index], names=[counts.index.name] + ) + + return counts + def nlargest(self, n, columns, keep="first") -> "DataFrame": """ Return the first `n` rows ordered by `columns` in descending order. @@ -5230,7 +5064,7 @@ def nsmallest(self, n, columns, keep="first") -> "DataFrame": Examples -------- >>> df = pd.DataFrame({'population': [59000000, 65000000, 434000, - ... 434000, 434000, 337000, 11300, + ... 434000, 434000, 337000, 337000, ... 11300, 11300], ... 'GDP': [1937894, 2583560 , 12011, 4520, 12128, ... 17036, 182, 38, 311], @@ -5247,18 +5081,18 @@ def nsmallest(self, n, columns, keep="first") -> "DataFrame": Maldives 434000 4520 MV Brunei 434000 12128 BN Iceland 337000 17036 IS - Nauru 11300 182 NR + Nauru 337000 182 NR Tuvalu 11300 38 TV Anguilla 11300 311 AI In the following example, we will use ``nsmallest`` to select the - three rows having the smallest values in column "a". + three rows having the smallest values in column "population". >>> df.nsmallest(3, 'population') - population GDP alpha-2 - Nauru 11300 182 NR - Tuvalu 11300 38 TV - Anguilla 11300 311 AI + population GDP alpha-2 + Tuvalu 11300 38 TV + Anguilla 11300 311 AI + Iceland 337000 17036 IS When using ``keep='last'``, ties are resolved in reverse order: @@ -5266,24 +5100,25 @@ def nsmallest(self, n, columns, keep="first") -> "DataFrame": population GDP alpha-2 Anguilla 11300 311 AI Tuvalu 11300 38 TV - Nauru 11300 182 NR + Nauru 337000 182 NR When using ``keep='all'``, all duplicate items are maintained: >>> df.nsmallest(3, 'population', keep='all') - population GDP alpha-2 - Nauru 11300 182 NR - Tuvalu 11300 38 TV - Anguilla 11300 311 AI + population GDP alpha-2 + Tuvalu 11300 38 TV + Anguilla 11300 311 AI + Iceland 337000 17036 IS + Nauru 337000 182 NR - To order by the largest values in column "a" and then "c", we can + To order by the smallest values in column "population" and then "GDP", we can specify multiple columns like in the next example. >>> df.nsmallest(3, ['population', 'GDP']) population GDP alpha-2 Tuvalu 11300 38 TV - Nauru 11300 182 NR Anguilla 11300 311 AI + Nauru 337000 182 NR """ return algorithms.SelectNFrame( self, n=n, keep=keep, columns=columns @@ -5897,11 +5732,19 @@ def groupby( Parameters ----------%s - index : str or object, optional + index : str or object or a list of str, optional Column to use to make new frame's index. If None, uses existing index. - columns : str or object + + .. versionchanged:: 1.1.0 + Also accept list of index names. + + columns : str or object or a list of str Column to use to make new frame's columns. + + .. versionchanged:: 1.1.0 + Also accept list of columns names. + values : str, object or a list of the previous, optional Column(s) to use for populating new frame's values. If not specified, all remaining columns will be used and the result will @@ -5968,6 +5811,38 @@ def groupby( one 1 2 3 x y z two 4 5 6 q w t + You could also assign a list of column names or a list of index names. + + >>> df = pd.DataFrame({ + ... "lev1": [1, 1, 1, 2, 2, 2], + ... "lev2": [1, 1, 2, 1, 1, 2], + ... "lev3": [1, 2, 1, 2, 1, 2], + ... "lev4": [1, 2, 3, 4, 5, 6], + ... "values": [0, 1, 2, 3, 4, 5]}) + >>> df + lev1 lev2 lev3 lev4 values + 0 1 1 1 1 0 + 1 1 1 2 2 1 + 2 1 2 1 3 2 + 3 2 1 2 4 3 + 4 2 1 1 5 4 + 5 2 2 2 6 5 + + >>> df.pivot(index="lev1", columns=["lev2", "lev3"],values="values") + lev2 1 2 + lev3 1 2 1 2 + lev1 + 1 0.0 1.0 2.0 NaN + 2 4.0 3.0 NaN 5.0 + + >>> df.pivot(index=["lev1", "lev2"], columns=["lev3"],values="values") + lev3 1 2 + lev1 lev2 + 1 1 0.0 1.0 + 2 2.0 NaN + 2 1 4.0 3.0 + 2 NaN 5.0 + A ValueError is raised if there are any duplicates. >>> df = pd.DataFrame({"foo": ['one', 'one', 'two', 'two'], @@ -7008,7 +6883,6 @@ def applymap(self, func) -> "DataFrame": 0 1.000000 4.494400 1 11.262736 20.857489 """ - # if we have a dtype == 'M8[ns]', provide boxed values def infer(x): if x.empty: @@ -7927,6 +7801,19 @@ def _count_level(self, level, axis=0, numeric_only=False): def _reduce( self, op, name, axis=0, skipna=True, numeric_only=None, filter_type=None, **kwds ): + + dtype_is_dt = self.dtypes.apply(lambda x: x.kind == "M") + if numeric_only is None and name in ["mean", "median"] and dtype_is_dt.any(): + warnings.warn( + "DataFrame.mean and DataFrame.median with numeric_only=None " + "will include datetime64 and datetime64tz columns in a " + "future version.", + FutureWarning, + stacklevel=3, + ) + cols = self.columns[~dtype_is_dt] + self = self[cols] + if axis is None and filter_type == "bool": labels = None constructor = None @@ -7966,9 +7853,15 @@ def _get_data(axis_matters): out_dtype = "bool" if filter_type == "bool" else None + def blk_func(values): + if values.ndim == 1 and not isinstance(values, np.ndarray): + # we can't pass axis=1 + return op(values, axis=0, skipna=skipna, **kwds) + return op(values, axis=1, skipna=skipna, **kwds) + # After possibly _get_data and transposing, we are now in the # simple case where we can use BlockManager._reduce - res = df._data.reduce(op, axis=1, skipna=skipna, **kwds) + res = df._data.reduce(blk_func) assert isinstance(res, dict) if len(res): assert len(res) == max(list(res.keys())) + 1, res.keys() @@ -8520,6 +8413,14 @@ def isin(self, values) -> "DataFrame": # ---------------------------------------------------------------------- # Add index and columns + _AXIS_ORDERS = ["index", "columns"] + _AXIS_NUMBERS = {"index": 0, "columns": 1} + _AXIS_NAMES = {0: "index", 1: "columns"} + _AXIS_REVERSED = True + _AXIS_LEN = len(_AXIS_ORDERS) + _info_axis_number = 1 + _info_axis_name = "columns" + index: "Index" = properties.AxisProperty( axis=1, doc="The index (row labels) of the DataFrame." ) @@ -8535,13 +8436,6 @@ def isin(self, values) -> "DataFrame": sparse = CachedAccessor("sparse", SparseFrameAccessor) -DataFrame._setup_axes( - ["index", "columns"], - docs={ - "index": "The index (row labels) of the DataFrame.", - "columns": "The column labels of the DataFrame.", - }, -) DataFrame._add_numeric_operations() DataFrame._add_series_or_dataframe_operations() @@ -8551,13 +8445,8 @@ def isin(self, values) -> "DataFrame": def _from_nested_dict(data): # TODO: this should be seriously cythonized - new_data = {} + new_data = collections.defaultdict(dict) for index, s in data.items(): for col, v in s.items(): - new_data[col] = new_data.get(col, {}) new_data[col][index] = v return new_data - - -def _put_str(s, space): - return str(s)[:space].ljust(space) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 313d40b575629..ff7c481d550d4 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -33,7 +33,6 @@ from pandas._libs import Timestamp, iNaT, lib from pandas._typing import ( Axis, - Dtype, FilePathOrBuffer, FrameOrSeries, JSONSerializable, @@ -45,7 +44,12 @@ from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError -from pandas.util._decorators import Appender, Substitution, rewrite_axis_style_signature +from pandas.util._decorators import ( + Appender, + Substitution, + doc, + rewrite_axis_style_signature, +) from pandas.util._validators import ( validate_bool_kwarg, validate_fillna_kwargs, @@ -195,22 +199,10 @@ class NDFrame(PandasObject, SelectionMixin, indexing.IndexingMixin): def __init__( self, data: BlockManager, - axes: Optional[List[Index]] = None, copy: bool = False, - dtype: Optional[Dtype] = None, attrs: Optional[Mapping[Optional[Hashable], Any]] = None, - fastpath: bool = False, ): - - if not fastpath: - if dtype is not None: - data = data.astype(dtype) - elif copy: - data = data.copy() - - if axes is not None: - for i, ax in enumerate(axes): - data = data.reindex_axis(ax, axis=i) + # copy kwarg is retained for mypy compat, is not used object.__setattr__(self, "_is_copy", None) object.__setattr__(self, "_data", data) @@ -221,12 +213,13 @@ def __init__( attrs = dict(attrs) object.__setattr__(self, "_attrs", attrs) - def _init_mgr(self, mgr, axes=None, dtype=None, copy=False): + @classmethod + def _init_mgr(cls, mgr, axes=None, dtype=None, copy=False): """ passed a manager and a axes dict """ for a, axe in axes.items(): if axe is not None: mgr = mgr.reindex_axis( - axe, axis=self._get_block_manager_axis(a), copy=False + axe, axis=cls._get_block_manager_axis(a), copy=False ) # make a copy if explicitly requested @@ -257,9 +250,9 @@ def attrs(self) -> Dict[Optional[Hashable], Any]: def attrs(self, value: Mapping[Optional[Hashable], Any]) -> None: self._attrs = dict(value) - def _validate_dtype(self, dtype): + @classmethod + def _validate_dtype(cls, dtype): """ validate the passed dtype """ - if dtype is not None: dtype = pandas_dtype(dtype) @@ -267,7 +260,7 @@ def _validate_dtype(self, dtype): if dtype.kind == "V": raise NotImplementedError( "compound dtypes are not implemented " - f"in the {type(self).__name__} constructor" + f"in the {cls.__name__} constructor" ) return dtype @@ -277,21 +270,24 @@ def _validate_dtype(self, dtype): @property def _constructor(self: FrameOrSeries) -> Type[FrameOrSeries]: - """Used when a manipulation result has the same dimensions as the + """ + Used when a manipulation result has the same dimensions as the original. """ raise AbstractMethodError(self) @property def _constructor_sliced(self): - """Used when a manipulation result has one lower dimension(s) as the + """ + Used when a manipulation result has one lower dimension(s) as the original, such as DataFrame single columns slicing. """ raise AbstractMethodError(self) @property def _constructor_expanddim(self): - """Used when a manipulation result has one higher dimension as the + """ + Used when a manipulation result has one higher dimension as the original, such as Series.to_frame() """ raise NotImplementedError @@ -311,38 +307,18 @@ def _constructor_expanddim(self): _info_axis_name: str _AXIS_LEN: int - @classmethod - def _setup_axes(cls, axes: List[str], docs: Dict[str, str]) -> None: - """ - Provide axes setup for the major PandasObjects. - - Parameters - ---------- - axes : the names of the axes in order (lowest to highest) - docs : docstrings for the axis properties - """ - info_axis = len(axes) - 1 - axes_are_reversed = len(axes) > 1 - - cls._AXIS_ORDERS = axes - cls._AXIS_NUMBERS = {a: i for i, a in enumerate(axes)} - cls._AXIS_LEN = len(axes) - cls._AXIS_NAMES = dict(enumerate(axes)) - cls._AXIS_REVERSED = axes_are_reversed - - cls._info_axis_number = info_axis - cls._info_axis_name = axes[info_axis] - def _construct_axes_dict(self, axes=None, **kwargs): """Return an axes dictionary for myself.""" d = {a: self._get_axis(a) for a in (axes or self._AXIS_ORDERS)} d.update(kwargs) return d + @classmethod def _construct_axes_from_arguments( - self, args, kwargs, require_all: bool = False, sentinel=None + cls, args, kwargs, require_all: bool = False, sentinel=None ): - """Construct and returns axes if supplied in args/kwargs. + """ + Construct and returns axes if supplied in args/kwargs. If require_all, raise if all axis arguments are not supplied return a tuple of (axes, kwargs). @@ -351,10 +327,9 @@ def _construct_axes_from_arguments( supplied; useful to distinguish when a user explicitly passes None in scenarios where None has special meaning. """ - # construct the args args = list(args) - for a in self._AXIS_ORDERS: + for a in cls._AXIS_ORDERS: # look for a argument by position if a not in kwargs: @@ -364,7 +339,7 @@ def _construct_axes_from_arguments( if require_all: raise TypeError("not enough/duplicate arguments specified!") - axes = {a: kwargs.pop(a, sentinel) for a in self._AXIS_ORDERS} + axes = {a: kwargs.pop(a, sentinel) for a in cls._AXIS_ORDERS} return axes, kwargs @classmethod @@ -510,7 +485,7 @@ def ndim(self) -> int: return self._data.ndim @property - def size(self): + def size(self) -> int: """ Return an int representing the number of elements in this object. @@ -576,9 +551,6 @@ def set_axis(self, labels, axis=0, inplace=False): See Also -------- %(klass)s.rename_axis : Alter the name of the index%(see_also_sub)s. - - Examples - -------- """ if inplace: setattr(self, self._get_axis_name(axis), labels) @@ -630,6 +602,10 @@ def droplevel(self: FrameOrSeries, level, axis=0) -> FrameOrSeries: of levels. axis : {0 or 'index', 1 or 'columns'}, default 0 + Axis along which the level(s) is removed: + + * 0 or 'index': remove level(s) in column. + * 1 or 'columns': remove level(s) in row. Returns ------- @@ -645,7 +621,7 @@ def droplevel(self: FrameOrSeries, level, axis=0) -> FrameOrSeries: ... ]).set_index([0, 1]).rename_axis(['a', 'b']) >>> df.columns = pd.MultiIndex.from_tuples([ - ... ('c', 'e'), ('d', 'f') + ... ('c', 'e'), ('d', 'f') ... ], names=['level_1', 'level_2']) >>> df @@ -664,7 +640,7 @@ def droplevel(self: FrameOrSeries, level, axis=0) -> FrameOrSeries: 6 7 8 10 11 12 - >>> df.droplevel('level2', axis=1) + >>> df.droplevel('level_2', axis=1) level_1 c d a b 1 2 3 4 @@ -896,7 +872,6 @@ def rename( Examples -------- - >>> s = pd.Series([1, 2, 3]) >>> s 0 1 @@ -1736,7 +1711,8 @@ def keys(self): return self._info_axis def items(self): - """Iterate over (label, values) on info axis + """ + Iterate over (label, values) on info axis This is index for Series and columns for DataFrame. @@ -2205,7 +2181,6 @@ def to_json( Examples -------- - >>> df = pd.DataFrame([['a', 'b'], ['c', 'd']], ... index=['row 1', 'row 2'], ... columns=['col 1', 'col 2']) @@ -2246,7 +2221,6 @@ def to_json( "data": [{"index": "row 1", "col 1": "a", "col 2": "b"}, {"index": "row 2", "col 1": "c", "col 2": "d"}]}' """ - from pandas.io import json if date_format is None and orient == "table": @@ -2440,7 +2414,7 @@ def to_sql( library. Legacy support is provided for sqlite3.Connection objects. The user is responsible for engine disposal and connection closure for the SQLAlchemy connectable See `here \ - `_ + `_. schema : str, optional Specify the schema (if database flavor supports this). If None, use @@ -2505,7 +2479,6 @@ def to_sql( Examples -------- - Create an in-memory SQLite database. >>> from sqlalchemy import create_engine @@ -3082,7 +3055,6 @@ def to_csv( >>> df.to_csv('out.zip', index=False, ... compression=compression_opts) # doctest: +SKIP """ - df = self if isinstance(self, ABCDataFrame) else self.to_frame() from pandas.io.formats.csvs import CSVFormatter @@ -3120,18 +3092,22 @@ def to_csv( # Lookup Caching def _set_as_cached(self, item, cacher) -> None: - """Set the _cacher attribute on the calling object with a weakref to + """ + Set the _cacher attribute on the calling object with a weakref to cacher. """ self._cacher = (item, weakref.ref(cacher)) def _reset_cacher(self) -> None: - """Reset the cacher.""" + """ + Reset the cacher. + """ if hasattr(self, "_cacher"): del self._cacher def _maybe_cache_changed(self, item, value) -> None: - """The object has called back to us saying maybe it has changed. + """ + The object has called back to us saying maybe it has changed. """ self._data.set(item, value) @@ -3161,7 +3137,6 @@ def _maybe_update_cacher( verify_is_copy : bool, default True Provide is_copy checks. """ - cacher = getattr(self, "_cacher", None) if cacher is not None: ref = cacher[1]() @@ -3291,9 +3266,7 @@ class max_speed ) return self._constructor(new_data).__finalize__(self) - def _take_with_is_copy( - self: FrameOrSeries, indices, axis=0, **kwargs - ) -> FrameOrSeries: + def _take_with_is_copy(self: FrameOrSeries, indices, axis=0) -> FrameOrSeries: """ Internal version of the `take` method that sets the `_is_copy` attribute to keep track of the parent dataframe (using in indexing @@ -3301,7 +3274,7 @@ def _take_with_is_copy( See the docstring of `take` for full explanation of the parameters. """ - result = self.take(indices=indices, axis=axis, **kwargs) + result = self.take(indices=indices, axis=axis) # Maybe set copy if we didn't actually change the index. if not result._get_axis(axis).equals(self._get_axis(axis)): result._set_is_copy(self) @@ -3486,26 +3459,25 @@ def _get_item_cache(self, item): res._is_copy = self._is_copy return res - def _iget_item_cache(self, item): + def _iget_item_cache(self, item: int): """Return the cached item, item represents a positional indexer.""" ax = self._info_axis if ax.is_unique: lower = self._get_item_cache(ax[item]) else: - lower = self._take_with_is_copy(item, axis=self._info_axis_number) + return self._ixs(item, axis=1) return lower def _box_item_values(self, key, values): raise AbstractMethodError(self) - def _slice( - self: FrameOrSeries, slobj: slice, axis=0, kind: str = "getitem" - ) -> FrameOrSeries: + def _slice(self: FrameOrSeries, slobj: slice, axis=0) -> FrameOrSeries: """ Construct a slice of this container. - kind parameter is maintained for compatibility with Series slicing. + Slicing with this method is *always* positional. """ + assert isinstance(slobj, slice), type(slobj) axis = self._get_block_manager_axis(axis) result = self._constructor(self._data.get_slice(slobj, axis=axis)) result = result.__finalize__(self) @@ -3577,7 +3549,6 @@ def _check_setitem_copy(self, stacklevel=4, t="setting", force=False): df.iloc[0:5]['group'] = 'a' """ - # return early if the check is not needed if not (force or self._is_copy): return @@ -3681,7 +3652,7 @@ def get(self, key, default=None): return default @property - def _is_view(self): + def _is_view(self) -> bool_t: """Return boolean indicating if self is view of another array """ return self._data.is_view @@ -4187,7 +4158,6 @@ def reindex(self: FrameOrSeries, *args, **kwargs) -> FrameOrSeries: Examples -------- - ``DataFrame.reindex`` supports two calling conventions * ``(index=index_labels, columns=column_labels, ...)`` @@ -4419,7 +4389,6 @@ def _reindex_with_indexers( allow_dups: bool_t = False, ) -> FrameOrSeries: """allow_dups indicates an internal call here """ - # reindex doing multiple operations on different axes if indicated new_data = self._data for axis in sorted(reindexers.keys()): @@ -4615,7 +4584,6 @@ def head(self: FrameOrSeries, n: int = 5) -> FrameOrSeries: 4 monkey 5 parrot """ - return self.iloc[:n] def tail(self: FrameOrSeries, n: int = 5) -> FrameOrSeries: @@ -4688,7 +4656,6 @@ def tail(self: FrameOrSeries, n: int = 5) -> FrameOrSeries: 7 whale 8 zebra """ - if n == 0: return self.iloc[0:0] return self.iloc[-n:] @@ -4803,7 +4770,6 @@ def sample( falcon 2 2 10 fish 0 0 8 """ - if axis is None: axis = self._stat_axis_number @@ -5086,10 +5052,10 @@ def __finalize__( return self def __getattr__(self, name: str): - """After regular attribute access, try looking up the name + """ + After regular attribute access, try looking up the name This allows simpler access to columns for interactive use. """ - # Note: obj.x will always call obj.__getattribute__('x') prior to # calling obj.__getattr__('x'). @@ -5105,10 +5071,10 @@ def __getattr__(self, name: str): return object.__getattribute__(self, name) def __setattr__(self, name: str, value) -> None: - """After regular attribute access, try setting the name + """ + After regular attribute access, try setting the name This allows simpler access to columns for interactive use. """ - # first try regular attribute access via __getattribute__, so that # e.g. ``obj.x`` and ``obj.x = 4`` will always reference/modify # the same attribute. @@ -5146,7 +5112,8 @@ def __setattr__(self, name: str, value) -> None: object.__setattr__(self, name, value) def _dir_additions(self): - """ add the string-like attributes from the info_axis. + """ + add the string-like attributes from the info_axis. If info_axis is a MultiIndex, it's first level values are used. """ additions = { @@ -5160,7 +5127,8 @@ def _dir_additions(self): # Consolidation of internals def _protect_consolidate(self, f): - """Consolidate _data -- if the blocks have changed, then clear the + """ + Consolidate _data -- if the blocks have changed, then clear the cache """ blocks_before = len(self._data.blocks) @@ -5200,18 +5168,17 @@ def _consolidate(self, inplace: bool_t = False): return self._constructor(cons_data).__finalize__(self) @property - def _is_mixed_type(self): + def _is_mixed_type(self) -> bool_t: f = lambda: self._data.is_mixed_type return self._protect_consolidate(f) @property - def _is_numeric_mixed_type(self): + def _is_numeric_mixed_type(self) -> bool_t: f = lambda: self._data.is_numeric_mixed_type return self._protect_consolidate(f) def _check_inplace_setting(self, value) -> bool_t: """ check whether we allow in-place setting with this type of value """ - if self._is_mixed_type: if not self._is_numeric_mixed_type: @@ -5777,7 +5744,6 @@ def convert_dtypes( Notes ----- - By default, ``convert_dtypes`` will attempt to convert a Series (or each Series in a DataFrame) to dtypes that support ``pd.NA``. By using the options ``convert_string``, ``convert_integer``, and ``convert_boolean``, it is @@ -5879,6 +5845,7 @@ def convert_dtypes( # ---------------------------------------------------------------------- # Filling NA's + @doc(**_shared_doc_kwargs) def fillna( self: FrameOrSeries, value=None, @@ -5899,11 +5866,11 @@ def fillna( each index (for a Series) or column (for a DataFrame). Values not in the dict/Series/DataFrame will not be filled. This value cannot be a list. - method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None + method : {{'backfill', 'bfill', 'pad', 'ffill', None}}, default None Method to use for filling holes in reindexed Series pad / ffill: propagate last valid observation forward to next valid backfill / bfill: use next valid observation to fill gap. - axis : %(axes_single_arg)s + axis : {axes_single_arg} Axis along which to fill missing values. inplace : bool, default False If True, fill in-place. Note: this will modify any @@ -5923,7 +5890,7 @@ def fillna( Returns ------- - %(klass)s or None + {klass} or None Object with missing values filled or None if ``inplace=True``. See Also @@ -5967,7 +5934,7 @@ def fillna( Replace all NaN elements in column 'A', 'B', 'C', and 'D', with 0, 1, 2, and 3 respectively. - >>> values = {'A': 0, 'B': 1, 'C': 2, 'D': 3} + >>> values = {{'A': 0, 'B': 1, 'C': 2, 'D': 3}} >>> df.fillna(value=values) A B C D 0 0.0 2.0 2.0 0 @@ -7442,7 +7409,6 @@ def asfreq( Examples -------- - Start by creating a series with 4 one minute timestamps. >>> index = pd.date_range('1/1/2000', periods=4, freq='T') @@ -7577,16 +7543,22 @@ def between_time( Parameters ---------- start_time : datetime.time or str + Initial time as a time filter limit. end_time : datetime.time or str + End time as a time filter limit. include_start : bool, default True + Whether the start time needs to be included in the result. include_end : bool, default True + Whether the end time needs to be included in the result. axis : {0 or 'index', 1 or 'columns'}, default 0 + Determine range time on index or columns value. .. versionadded:: 0.24.0 Returns ------- Series or DataFrame + Data from the original object filtered to the specified dates range. Raises ------ @@ -7721,7 +7693,6 @@ def resample( Examples -------- - Start by creating a series with 9 one minute timestamps. >>> index = pd.date_range('1/1/2000', periods=9, freq='T') @@ -7918,7 +7889,6 @@ def resample( 2000-01-03 32 150 2000-01-04 36 90 """ - from pandas.core.resample import get_resampler axis = self._get_axis_number(axis) @@ -8109,7 +8079,6 @@ def rank( Examples -------- - >>> df = pd.DataFrame(data={'Animal': ['cat', 'penguin', 'dog', ... 'spider', 'snake'], ... 'Number_legs': [4, 2, 4, 8, np.nan]}) @@ -8360,9 +8329,7 @@ def _align_frame( left = self._ensure_type( left.fillna(method=method, axis=fill_axis, limit=limit) ) - right = self._ensure_type( - right.fillna(method=method, axis=fill_axis, limit=limit) - ) + right = right.fillna(method=method, axis=fill_axis, limit=limit) # if DatetimeIndex have different tz, convert to UTC if is_datetime64tz_dtype(left.index): @@ -8934,7 +8901,6 @@ def tshift( attributes of the index. If neither of those attributes exist, a ValueError is thrown """ - index = self._get_axis(axis) if freq is None: freq = getattr(index, "freq", None) @@ -9247,7 +9213,6 @@ def tz_localize( Examples -------- - Localize local times: >>> s = pd.Series([1], @@ -9923,7 +9888,6 @@ def _add_numeric_operations(cls): """ Add the operations to the cls; evaluate the doc strings again """ - axis_descr, name, name2 = _doc_parms(cls) cls.any = _make_logical_function( @@ -9961,7 +9925,7 @@ def _add_numeric_operations(cls): see_also="", examples="", ) - @Appender(_num_doc) + @Appender(_num_doc_mad) def mad(self, axis=None, skipna=None, level=None): if skipna is None: skipna = True @@ -10161,7 +10125,6 @@ def _add_series_or_dataframe_operations(cls): Add the series or dataframe only operations to the cls; evaluate the doc strings again. """ - from pandas.core.window import EWM, Expanding, Rolling, Window @Appender(Rolling.__doc__) @@ -10275,7 +10238,6 @@ def _find_valid_index(self, how: str): ------- idx_first_valid : type of index """ - idxpos = find_valid_index(self._values, how) if idxpos is None: return None @@ -10330,6 +10292,26 @@ def _doc_parms(cls): %(examples)s """ +_num_doc_mad = """ +%(desc)s + +Parameters +---------- +axis : %(axis_descr)s + Axis for the function to be applied on. +skipna : bool, default None + Exclude NA/null values when computing the result. +level : int or level name, default None + If the axis is a MultiIndex (hierarchical), count along a + particular level, collapsing into a %(name1)s. + +Returns +------- +%(name1)s or %(name2)s (if level specified)\ +%(see_also)s\ +%(examples)s +""" + _num_ddof_doc = """ %(desc)s diff --git a/pandas/core/groupby/categorical.py b/pandas/core/groupby/categorical.py index 399ed9ddc9ba1..c71ebee397bbd 100644 --- a/pandas/core/groupby/categorical.py +++ b/pandas/core/groupby/categorical.py @@ -41,7 +41,6 @@ def recode_for_groupby(c: Categorical, sort: bool, observed: bool): Categorical or None If we are observed, return the original categorical, otherwise None """ - # we only care about observed values if observed: unique_codes = unique1d(c.codes) @@ -90,7 +89,6 @@ def recode_from_groupby(c: Categorical, sort: bool, ci): ------- CategoricalIndex """ - # we re-order to the original category orderings if sort: return ci.set_categories(c.categories) diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index f194c774cf329..1bb512aee39e2 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -591,30 +591,18 @@ def nunique(self, dropna: bool = True) -> Series: val = self.obj._internal_get_values() - # GH 27951 - # temporary fix while we wait for NumPy bug 12629 to be fixed - val[isna(val)] = np.datetime64("NaT") - - try: - sorter = np.lexsort((val, ids)) - except TypeError: # catches object dtypes - msg = f"val.dtype must be object, got {val.dtype}" - assert val.dtype == object, msg - val, _ = algorithms.factorize(val, sort=False) - sorter = np.lexsort((val, ids)) - _isna = lambda a: a == -1 - else: - _isna = isna - - ids, val = ids[sorter], val[sorter] + codes, _ = algorithms.factorize(val, sort=False) + sorter = np.lexsort((codes, ids)) + codes = codes[sorter] + ids = ids[sorter] # group boundaries are where group ids change # unique observations are where sorted values change idx = np.r_[0, 1 + np.nonzero(ids[1:] != ids[:-1])[0]] - inc = np.r_[1, val[1:] != val[:-1]] + inc = np.r_[1, codes[1:] != codes[:-1]] # 1st item of each group is a new unique observation - mask = _isna(val) + mask = codes == -1 if dropna: inc[idx] = 1 inc[mask] = 0 @@ -1571,7 +1559,6 @@ def filter(self, func, dropna=True, *args, **kwargs): 3 bar 4 1.0 5 bar 6 9.0 """ - indices = [] obj = self._selected_obj @@ -1626,7 +1613,6 @@ def _gotitem(self, key, ndim: int, subset=None): subset : object, default None subset to act on """ - if ndim == 2: if subset is None: subset = self.obj @@ -1844,7 +1830,6 @@ def nunique(self, dropna: bool = True): 4 ham 5 x 5 ham 5 y """ - obj = self._selected_obj def groupby_series(obj, col=None): diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 0245b9f74d944..f946f0e63a583 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -923,17 +923,10 @@ def _python_agg_general(self, func, *args, **kwargs): try: # if this function is invalid for this dtype, we will ignore it. - func(obj[:0]) + result, counts = self.grouper.agg_series(obj, f) except TypeError: continue - except AssertionError: - raise - except Exception: - # Our function depends on having a non-empty argument - # See test_groupby_agg_err_catching - pass - - result, counts = self.grouper.agg_series(obj, f) + assert result is not None key = base.OutputKey(label=name, position=idx) output[key] = self._try_cast(result, obj, numeric_only=True) @@ -1174,7 +1167,6 @@ def count(self): Series or DataFrame Count of values within each group. """ - # defined here for API doc raise NotImplementedError @@ -1277,7 +1269,6 @@ def std(self, ddof: int = 1): Series or DataFrame Standard deviation of values within each group. """ - # TODO: implement at Cython level? return np.sqrt(self.var(ddof=ddof)) @@ -1449,7 +1440,7 @@ def last(x): @Appender(_common_see_also) def ohlc(self) -> DataFrame: """ - Compute sum of values, excluding missing values. + Compute open, high, low and close values of a group, excluding missing values. For multiple groupings, the result index will be a MultiIndex @@ -1458,7 +1449,6 @@ def ohlc(self) -> DataFrame: DataFrame Open, high, low and close values within each group. """ - return self._apply_to_column_groupbys(lambda x: x._cython_agg_general("ohlc")) @Appender(DataFrame.describe.__doc__) @@ -1764,7 +1754,6 @@ def nth(self, n: Union[int, List[int]], dropna: Optional[str] = None) -> DataFra 1 1 2.0 4 2 5.0 """ - valid_containers = (set, list, tuple) if not isinstance(n, (valid_containers, int)): raise TypeError("n needs to be an int or a list/set/tuple of ints") @@ -1999,7 +1988,6 @@ def ngroup(self, ascending: bool = True): Examples -------- - >>> df = pd.DataFrame({"A": list("aaabba")}) >>> df A @@ -2034,7 +2022,6 @@ def ngroup(self, ascending: bool = True): 5 0 dtype: int64 """ - with _group_selection_context(self): index = self._selected_obj.index result = Series(self.grouper.group_info[0], index) @@ -2067,7 +2054,6 @@ def cumcount(self, ascending: bool = True): Examples -------- - >>> df = pd.DataFrame([['a'], ['a'], ['a'], ['b'], ['b'], ['a']], ... columns=['A']) >>> df @@ -2095,7 +2081,6 @@ def cumcount(self, ascending: bool = True): 5 0 dtype: int64 """ - with _group_selection_context(self): index = self._selected_obj.index cumcounts = self._cumcount_array(ascending=ascending) @@ -2336,7 +2321,8 @@ def shift(self, periods=1, freq=None, axis=0, fill_value=None): ---------- periods : int, default 1 Number of periods to shift. - freq : frequency string + freq : str, optional + Frequency string. axis : axis to shift, default 0 fill_value : optional @@ -2347,7 +2333,6 @@ def shift(self, periods=1, freq=None, axis=0, fill_value=None): Series or DataFrame Object shifted within each group. """ - if freq is not None or axis != 0 or not isna(fill_value): return self.apply(lambda x: x.shift(periods, freq, axis, fill_value)) diff --git a/pandas/core/groupby/grouper.py b/pandas/core/groupby/grouper.py index f0c6eedf5cee4..21e171f937de8 100644 --- a/pandas/core/groupby/grouper.py +++ b/pandas/core/groupby/grouper.py @@ -77,7 +77,6 @@ class Grouper: Examples -------- - Syntactic sugar for ``df.groupby('A')`` >>> df.groupby(Grouper(key='A')) @@ -130,7 +129,6 @@ def _get_grouper(self, obj, validate: bool = True): ------- a tuple of binner, grouper, obj (possibly sorted) """ - self._set_grouper(obj) self.grouper, _, self.obj = get_grouper( self.obj, diff --git a/pandas/core/groupby/ops.py b/pandas/core/groupby/ops.py index 761353ca5a6ca..7259268ac3f2b 100644 --- a/pandas/core/groupby/ops.py +++ b/pandas/core/groupby/ops.py @@ -169,7 +169,7 @@ def apply(self, f, data: FrameOrSeries, axis: int = 0): and not sdata.index._has_complex_internals ): try: - result_values, mutated = splitter.fast_apply(f, group_keys) + result_values, mutated = splitter.fast_apply(f, sdata, group_keys) except libreduction.InvalidApply as err: # This Exception is raised if `f` triggers an exception @@ -433,7 +433,6 @@ def _cython_operation( Names is only useful when dealing with 2D results, like ohlc (see self._name_functions). """ - assert kind in ["transform", "aggregate"] orig_values = values @@ -658,7 +657,7 @@ def _aggregate_series_fast(self, obj: Series, func): group_index, _, ngroups = self.group_info # avoids object / Series creation overhead - dummy = obj._get_values(slice(None, 0)) + dummy = obj.iloc[:0] indexer = get_group_index_sorter(group_index, ngroups) obj = obj.take(indexer) group_index = algorithms.take_nd(group_index, indexer, allow_fill=False) @@ -748,7 +747,6 @@ def __init__( @cache_readonly def groups(self): """ dict {group name -> group labels} """ - # this is mainly for compat # GH 3881 result = { @@ -780,7 +778,11 @@ def get_iterator(self, data: FrameOrSeries, axis: int = 0): Generator yielding sequence of (name, subsetted object) for each group """ - slicer = lambda start, edge: data._slice(slice(start, edge), axis=axis) + if axis == 0: + slicer = lambda start, edge: data.iloc[start:edge] + else: + slicer = lambda start, edge: data.iloc[:, start:edge] + length = len(data.axes[axis]) start = 0 @@ -919,29 +921,29 @@ def _chop(self, sdata, slice_obj: slice) -> NDFrame: class SeriesSplitter(DataSplitter): def _chop(self, sdata: Series, slice_obj: slice) -> Series: - return sdata._get_values(slice_obj) + return sdata.iloc[slice_obj] class FrameSplitter(DataSplitter): - def fast_apply(self, f, names): + def fast_apply(self, f, sdata: FrameOrSeries, names): # must return keys::list, values::list, mutated::bool starts, ends = lib.generate_slices(self.slabels, self.ngroups) - - sdata = self._get_sorted_data() return libreduction.apply_frame_axis0(sdata, f, names, starts, ends) def _chop(self, sdata: DataFrame, slice_obj: slice) -> DataFrame: if self.axis == 0: return sdata.iloc[slice_obj] else: - return sdata._slice(slice_obj, axis=1) + return sdata.iloc[:, slice_obj] -def get_splitter(data: FrameOrSeries, *args, **kwargs) -> DataSplitter: +def get_splitter( + data: FrameOrSeries, labels: np.ndarray, ngroups: int, axis: int = 0 +) -> DataSplitter: if isinstance(data, Series): klass: Type[DataSplitter] = SeriesSplitter else: # i.e. DataFrame klass = FrameSplitter - return klass(data, *args, **kwargs) + return klass(data, labels, ngroups, axis) diff --git a/pandas/core/indexers.py b/pandas/core/indexers.py index fe475527f4596..5e53b061dd1c8 100644 --- a/pandas/core/indexers.py +++ b/pandas/core/indexers.py @@ -10,6 +10,7 @@ from pandas.core.dtypes.common import ( is_array_like, is_bool_dtype, + is_extension_array_dtype, is_integer_dtype, is_list_like, ) @@ -219,7 +220,7 @@ def maybe_convert_indices(indices, n: int): def length_of_indexer(indexer, target=None) -> int: """ - Return the length of a single non-tuple indexer which could be a slice. + Return the expected length of target[indexer] Returns ------- @@ -245,6 +246,12 @@ def length_of_indexer(indexer, target=None) -> int: step = -step return (stop - start + step - 1) // step elif isinstance(indexer, (ABCSeries, ABCIndexClass, np.ndarray, list)): + if isinstance(indexer, list): + indexer = np.array(indexer) + + if indexer.dtype == bool: + # GH#25774 + return indexer.sum() return len(indexer) elif not is_list_like_indexer(indexer): return 1 @@ -270,6 +277,33 @@ def deprecate_ndim_indexing(result): ) +def unpack_1tuple(tup): + """ + If we have a length-1 tuple/list that contains a slice, unpack to just + the slice. + + Notes + ----- + The list case is deprecated. + """ + if len(tup) == 1 and isinstance(tup[0], slice): + # if we don't have a MultiIndex, we may still be able to handle + # a 1-tuple. see test_1tuple_without_multiindex + + if isinstance(tup, list): + # GH#31299 + warnings.warn( + "Indexing with a single-item list containing a " + "slice is deprecated and will raise in a future " + "version. Pass a tuple instead.", + FutureWarning, + stacklevel=3, + ) + + return tup[0] + return tup + + # ----------------------------------------------------------- # Public indexer validation @@ -296,7 +330,7 @@ def check_array_indexer(array: AnyArrayLike, indexer: Any) -> Any: indexer : array-like or list-like The array-like that's used to index. List-like input that is not yet a numpy array or an ExtensionArray is converted to one. Other input - types are passed through as is + types are passed through as is. Returns ------- @@ -333,14 +367,11 @@ def check_array_indexer(array: AnyArrayLike, indexer: Any) -> Any: ... IndexError: Boolean index has wrong length: 3 instead of 2. - A ValueError is raised when the mask cannot be converted to - a bool-dtype ndarray. + NA values in a boolean array are treated as False. >>> mask = pd.array([True, pd.NA]) >>> pd.api.indexers.check_array_indexer(arr, mask) - Traceback (most recent call last): - ... - ValueError: Cannot mask with a boolean indexer containing NA values + array([ True, False]) A numpy boolean mask will get passed through (if the length is correct): @@ -392,10 +423,10 @@ def check_array_indexer(array: AnyArrayLike, indexer: Any) -> Any: dtype = indexer.dtype if is_bool_dtype(dtype): - try: + if is_extension_array_dtype(dtype): + indexer = indexer.to_numpy(dtype=bool, na_value=False) + else: indexer = np.asarray(indexer, dtype=bool) - except ValueError: - raise ValueError("Cannot mask with a boolean indexer containing NA values") # GH26658 if len(indexer) != len(array): diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index e8ad2bef099a1..5b674458e95ee 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -8,7 +8,7 @@ from pandas._libs import algos as libalgos, index as libindex, lib import pandas._libs.join as libjoin -from pandas._libs.lib import is_datetime_array +from pandas._libs.lib import is_datetime_array, no_default from pandas._libs.tslibs import OutOfBoundsDatetime, Timestamp from pandas._libs.tslibs.period import IncompatibleFrequency from pandas._libs.tslibs.timezones import tz_compare @@ -304,21 +304,14 @@ def __new__( # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423 from pandas.core.indexes.category import CategoricalIndex - return CategoricalIndex(data, dtype=dtype, copy=copy, name=name, **kwargs) + return _maybe_asobject(dtype, CategoricalIndex, data, copy, name, **kwargs) # interval elif is_interval_dtype(data) or is_interval_dtype(dtype): # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423 from pandas.core.indexes.interval import IntervalIndex - closed = kwargs.pop("closed", None) - if is_dtype_equal(_o_dtype, dtype): - return IntervalIndex( - data, name=name, copy=copy, closed=closed, **kwargs - ).astype(object) - return IntervalIndex( - data, dtype=dtype, name=name, copy=copy, closed=closed, **kwargs - ) + return _maybe_asobject(dtype, IntervalIndex, data, copy, name, **kwargs) elif ( is_datetime64_any_dtype(data) @@ -328,39 +321,19 @@ def __new__( # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423 from pandas import DatetimeIndex - if is_dtype_equal(_o_dtype, dtype): - # GH#23524 passing `dtype=object` to DatetimeIndex is invalid, - # will raise in the where `data` is already tz-aware. So - # we leave it out of this step and cast to object-dtype after - # the DatetimeIndex construction. - # Note we can pass copy=False because the .astype below - # will always make a copy - return DatetimeIndex(data, copy=False, name=name, **kwargs).astype( - object - ) - else: - return DatetimeIndex(data, copy=copy, name=name, dtype=dtype, **kwargs) + return _maybe_asobject(dtype, DatetimeIndex, data, copy, name, **kwargs) elif is_timedelta64_dtype(data) or is_timedelta64_dtype(dtype): # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423 from pandas import TimedeltaIndex - if is_dtype_equal(_o_dtype, dtype): - # Note we can pass copy=False because the .astype below - # will always make a copy - return TimedeltaIndex(data, copy=False, name=name, **kwargs).astype( - object - ) - else: - return TimedeltaIndex(data, copy=copy, name=name, dtype=dtype, **kwargs) + return _maybe_asobject(dtype, TimedeltaIndex, data, copy, name, **kwargs) elif is_period_dtype(data) or is_period_dtype(dtype): # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423 from pandas import PeriodIndex - if is_dtype_equal(_o_dtype, dtype): - return PeriodIndex(data, copy=False, name=name, **kwargs).astype(object) - return PeriodIndex(data, dtype=dtype, copy=copy, name=name, **kwargs) + return _maybe_asobject(dtype, PeriodIndex, data, copy, name, **kwargs) # extension dtype elif is_extension_array_dtype(data) or is_extension_array_dtype(dtype): @@ -478,7 +451,7 @@ def asi8(self): return None @classmethod - def _simple_new(cls, values, name=None, dtype=None): + def _simple_new(cls, values, name: Label = None): """ We require that we have a dtype compat for the values. If we are passed a non-dtype compat, then coerce using the constructor. @@ -512,7 +485,7 @@ def _get_attributes_dict(self): """ return {k: getattr(self, k, None) for k in self._attributes} - def _shallow_copy(self, values=None, **kwargs): + def _shallow_copy(self, values=None, name: Label = no_default): """ Create a new Index with the same class as the caller, don't copy the data, use the same object attributes with passed in attributes taking @@ -523,16 +496,14 @@ def _shallow_copy(self, values=None, **kwargs): Parameters ---------- values : the values to create the new Index, optional - kwargs : updates the default attributes for this Index + name : Label, defaults to self.name """ + name = self.name if name is no_default else name + if values is None: values = self.values - attributes = self._get_attributes_dict() - - attributes.update(kwargs) - - return self._simple_new(values, **attributes) + return self._simple_new(values, name=name) def _shallow_copy_with_infer(self, values, **kwargs): """ @@ -823,20 +794,26 @@ def repeat(self, repeats, axis=None): # -------------------------------------------------------------------- # Copying Methods - def copy(self, name=None, deep=False, dtype=None, **kwargs): + def copy(self, name=None, deep=False, dtype=None, names=None): """ - Make a copy of this object. Name and dtype sets those attributes on - the new object. + Make a copy of this object. + + Name and dtype sets those attributes on the new object. Parameters ---------- - name : str, optional + name : Label, optional + Set name for new object. deep : bool, default False - dtype : numpy dtype or pandas type + dtype : numpy dtype or pandas type, optional + Set dtype for new object. + names : list-like, optional + Kept for compatibility with MultiIndex. Should not be used. Returns ------- - copy : Index + Index + Index refer to new object which is a copy of this object. Notes ----- @@ -848,7 +825,6 @@ def copy(self, name=None, deep=False, dtype=None, **kwargs): else: new_index = self._shallow_copy() - names = kwargs.get("names") names = self._validate_names(name=name, names=names, deep=deep) new_index = new_index.set_names(names) @@ -911,7 +887,6 @@ def _format_data(self, name=None) -> str_t: """ Return the formatted data as a unicode string. """ - # do we want to justify (only do so for non-objects) is_justify = True @@ -1002,7 +977,6 @@ def to_native_types(self, slicer=None, **kwargs): numpy.ndarray Formatted values. """ - values = self if slicer is not None: values = values[slicer] @@ -1091,7 +1065,6 @@ def to_series(self, index=None, name=None): Series The dtype will be based on the type of the Index values. """ - from pandas import Series if index is None: @@ -1152,7 +1125,6 @@ def to_frame(self, index: bool = True, name=None): 1 Bear 2 Cow """ - from pandas import DataFrame if name is None: @@ -1293,7 +1265,6 @@ def set_names(self, names, level=None, inplace: bool = False): ( 'cobra', 2019)], names=['species', 'year']) """ - if level is not None and not isinstance(self, ABCMultiIndex): raise ValueError("Level must be None for non-MultiIndex") @@ -1456,7 +1427,6 @@ def _get_level_values(self, level): Examples -------- - >>> idx = pd.Index(list('abc')) >>> idx Index(['a', 'b', 'c'], dtype='object') @@ -2505,7 +2475,6 @@ def union(self, other, sort=None): Examples -------- - Union matching dtypes >>> idx1 = pd.Index([1, 2, 3, 4]) @@ -2547,7 +2516,6 @@ def _union(self, other, sort): ------- Index """ - if not len(other) or self.equals(other): return self._get_reconciled_name_object(other) @@ -2637,7 +2605,6 @@ def intersection(self, other, sort=False): Examples -------- - >>> idx1 = pd.Index([1, 2, 3, 4]) >>> idx2 = pd.Index([3, 4, 5, 6]) >>> idx1.intersection(idx2) @@ -2718,7 +2685,6 @@ def difference(self, other, sort=None): Examples -------- - >>> idx1 = pd.Index([2, 1, 3, 4]) >>> idx2 = pd.Index([3, 4, 5, 6]) >>> idx1.difference(idx2) @@ -3100,6 +3066,16 @@ def _filter_indexer_tolerance( # -------------------------------------------------------------------- # Indexer Conversion Methods + def _get_partial_string_timestamp_match_key(self, key): + """ + Translate any partial string timestamp matches in key, returning the + new key. + + Only relevant for MultiIndex. + """ + # GH#10331 + return key + def _convert_scalar_indexer(self, key, kind: str_t): """ Convert a scalar indexer. @@ -3120,7 +3096,7 @@ def _convert_scalar_indexer(self, key, kind: str_t): if kind == "getitem" and is_float(key): if not self.is_floating(): - self._invalid_indexer("label", key) + raise KeyError(key) elif kind == "loc" and is_float(key): @@ -3134,15 +3110,24 @@ def _convert_scalar_indexer(self, key, kind: str_t): "string", "mixed", ]: - self._invalid_indexer("label", key) + raise KeyError(key) elif kind == "loc" and is_integer(key): - if not self.holds_integer(): - self._invalid_indexer("label", key) + if not (is_integer_dtype(self.dtype) or is_object_dtype(self.dtype)): + raise KeyError(key) return key - def _convert_slice_indexer(self, key: slice, kind=None): + def _validate_positional_slice(self, key: slice): + """ + For positional indexing, a slice must have either int or None + for each of start, stop, and step. + """ + self._validate_indexer("positional", key.start, "iloc") + self._validate_indexer("positional", key.stop, "iloc") + self._validate_indexer("positional", key.step, "iloc") + + def _convert_slice_indexer(self, key: slice, kind: str_t): """ Convert a slice indexer. @@ -3152,16 +3137,9 @@ def _convert_slice_indexer(self, key: slice, kind=None): Parameters ---------- key : label of the slice bound - kind : {'loc', 'getitem', 'iloc'} or None + kind : {'loc', 'getitem'} """ - assert kind in ["loc", "getitem", "iloc", None] - - # validate iloc - if kind == "iloc": - self._validate_indexer("positional", key.start, "iloc") - self._validate_indexer("positional", key.stop, "iloc") - self._validate_indexer("positional", key.step, "iloc") - return key + assert kind in ["loc", "getitem"], kind # potentially cast the bounds to integers start, stop, step = key.start, key.stop, key.step @@ -3170,8 +3148,7 @@ def _convert_slice_indexer(self, key: slice, kind=None): def is_int(v): return v is None or is_integer(v) - is_null_slicer = start is None and stop is None - is_index_slice = is_int(start) and is_int(stop) + is_index_slice = is_int(start) and is_int(stop) and is_int(step) is_positional = is_index_slice and not ( self.is_integer() or self.is_categorical() ) @@ -3190,19 +3167,18 @@ def is_int(v): # convert the slice to an indexer here # if we are mixed and have integers - try: - if is_positional and self.is_mixed(): + if is_positional and self.is_mixed(): + try: # Validate start & stop if start is not None: self.get_loc(start) if stop is not None: self.get_loc(stop) is_positional = False - except KeyError: - if self.inferred_type in ["mixed-integer-float", "integer-na"]: - raise + except KeyError: + pass - if is_null_slicer: + if com.is_null_slice(key): indexer = key elif is_positional: indexer = key @@ -3304,7 +3280,6 @@ def _can_reindex(self, indexer): ------ ValueError if its a duplicate axis """ - # trying to reindex on an axis with duplicates if not self.is_unique and len(indexer): raise ValueError("cannot reindex from a duplicate axis") @@ -3339,7 +3314,7 @@ def reindex(self, target, method=None, level=None, limit=None, tolerance=None): values = range(0) else: values = self._data[:0] # appropriately-dtyped empty array - target = self._simple_new(values, dtype=self.dtype, **attrs) + target = self._simple_new(values, **attrs) else: target = ensure_index(target) @@ -3389,7 +3364,6 @@ def _reindex_non_unique(self, target): Indices of output values in original index. """ - target = ensure_index(target) indexer, missing = self.get_indexer_non_unique(target) check = indexer != -1 @@ -3963,18 +3937,35 @@ def memory_usage(self, deep: bool = False) -> int: def where(self, cond, other=None): """ - Return an Index of same shape as self and whose corresponding - entries are from self where cond is True and otherwise are from - other. + Replace values where the condition is False. + + The replacement is taken from other. Parameters ---------- cond : bool array-like with the same length as self - other : scalar, or array-like + Condition to select the values on. + other : scalar, or array-like, default None + Replacement if the condition is False. Returns ------- - Index + pandas.Index + A copy of self with values replaced from other + where the condition is False. + + See Also + -------- + Series.where : Same method for Series. + DataFrame.where : Same method for DataFrame. + + Examples + -------- + >>> idx = pd.Index(['car', 'bike', 'train', 'tractor']) + >>> idx + Index(['car', 'bike', 'train', 'tractor'], dtype='object') + >>> idx.where(idx.isin(['car', 'train']), 'other') + Index(['car', 'other', 'train', 'other'], dtype='object') """ if other is None: other = self._na_value @@ -4180,7 +4171,6 @@ def append(self, other): ------- appended : Index """ - to_concat = [self] if isinstance(other, (list, tuple)): @@ -4597,7 +4587,7 @@ def get_value(self, series: "Series", key): else: raise - return self._get_values_for_loc(series, loc) + return self._get_values_for_loc(series, loc, key) def _should_fallback_to_positional(self) -> bool: """ @@ -4607,12 +4597,14 @@ def _should_fallback_to_positional(self) -> bool: return False return True - def _get_values_for_loc(self, series: "Series", loc): + def _get_values_for_loc(self, series: "Series", loc, key): """ Do a positional lookup on the given Series, returning either a scalar or a Series. Assumes that `series.index is self` + + key is included for MultiIndex compat. """ if is_integer(loc): return series._values[loc] @@ -4721,7 +4713,6 @@ def groupby(self, values) -> PrettyDict[Hashable, np.ndarray]: dict {group name -> group labels} """ - # TODO: if we are a MultiIndex, we can do better # that converting to tuples if isinstance(values, ABCMultiIndex): @@ -4753,7 +4744,6 @@ def map(self, mapper, na_action=None): If the function returns a tuple with more than one element a MultiIndex will be returned. """ - from pandas.core.indexes.multi import MultiIndex new_values = super()._map_values(mapper, na_action=na_action) @@ -4919,7 +4909,6 @@ def _maybe_cast_indexer(self, key): If we have a float key and are not a floating index, then try to cast to an int if equivalent. """ - if not self.is_floating(): return com.cast_scalar_indexer(key) return key @@ -5147,9 +5136,30 @@ def delete(self, loc): """ Make new Index with passed location(-s) deleted. + Parameters + ---------- + loc : int or list of int + Location of item(-s) which will be deleted. + Use a list of locations to delete more than one value at the same time. + Returns ------- - new_index : Index + Index + New Index with passed location(-s) deleted. + + See Also + -------- + numpy.delete : Delete any rows and column from NumPy array (ndarray). + + Examples + -------- + >>> idx = pd.Index(['a', 'b', 'c']) + >>> idx.delete(1) + Index(['a', 'c'], dtype='object') + + >>> idx = pd.Index(['a', 'b', 'c']) + >>> idx.delete([0, 2]) + Index(['b'], dtype='object') """ return self._shallow_copy(np.delete(self._data, loc)) @@ -5736,7 +5746,6 @@ def _try_convert_to_int_array( ------ ValueError if the conversion was not successful. """ - if not is_unsigned_integer_dtype(dtype): # skip int64 conversion attempt if uint-like dtype is passed, as # this could return Int64Index when UInt64Index is what's desired @@ -5757,3 +5766,40 @@ def _try_convert_to_int_array( pass raise ValueError + + +def _maybe_asobject(dtype, klass, data, copy: bool, name: Label, **kwargs): + """ + If an object dtype was specified, create the non-object Index + and then convert it to object. + + Parameters + ---------- + dtype : np.dtype, ExtensionDtype, str + klass : Index subclass + data : list-like + copy : bool + name : hashable + **kwargs + + Returns + ------- + Index + + Notes + ----- + We assume that calling .astype(object) on this klass will make a copy. + """ + + # GH#23524 passing `dtype=object` to DatetimeIndex is invalid, + # will raise in the where `data` is already tz-aware. So + # we leave it out of this step and cast to object-dtype after + # the DatetimeIndex construction. + + if is_dtype_equal(_o_dtype, dtype): + # Note we can pass copy=False because the .astype below + # will always make a copy + index = klass(data, copy=False, name=name, **kwargs) + return index.astype(object) + + return klass(data, dtype=dtype, copy=copy, name=name, **kwargs) diff --git a/pandas/core/indexes/category.py b/pandas/core/indexes/category.py index 85229c728848f..8c2d7f4aa6c0e 100644 --- a/pandas/core/indexes/category.py +++ b/pandas/core/indexes/category.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, List +from typing import Any, List import warnings import numpy as np @@ -7,6 +7,8 @@ from pandas._libs import index as libindex from pandas._libs.hashtable import duplicated_int64 +from pandas._libs.lib import no_default +from pandas._typing import Label from pandas.util._decorators import Appender, cache_readonly from pandas.core.dtypes.common import ( @@ -17,7 +19,6 @@ is_scalar, ) from pandas.core.dtypes.dtypes import CategoricalDtype -from pandas.core.dtypes.generic import ABCCategorical, ABCSeries from pandas.core.dtypes.missing import isna from pandas.core import accessor @@ -28,9 +29,7 @@ from pandas.core.indexes.base import Index, _index_shared_docs, maybe_extract_name from pandas.core.indexes.extension import ExtensionIndex, inherit_names import pandas.core.missing as missing - -if TYPE_CHECKING: - from pandas import Series +from pandas.core.ops import get_op_result_name _index_doc_kwargs = dict(ibase._index_doc_kwargs) _index_doc_kwargs.update(dict(target_klass="CategoricalIndex")) @@ -196,7 +195,9 @@ def __new__( raise cls._scalar_data_error(data) data = [] - data = cls._create_categorical(data, dtype=dtype) + assert isinstance(dtype, CategoricalDtype), dtype + if not isinstance(data, Categorical) or data.dtype != dtype: + data = Categorical(data, dtype=dtype) data = data.copy() if copy else data @@ -218,7 +219,6 @@ def _create_from_codes(self, codes, dtype=None, name=None): ------- CategoricalIndex """ - if dtype is None: dtype = self.dtype if name is None: @@ -227,37 +227,10 @@ def _create_from_codes(self, codes, dtype=None, name=None): return CategoricalIndex(cat, name=name) @classmethod - def _create_categorical(cls, data, dtype=None): - """ - *this is an internal non-public method* - - create the correct categorical from data and the properties - - Parameters - ---------- - data : data for new Categorical - dtype : CategoricalDtype, defaults to existing - - Returns - ------- - Categorical - """ - if isinstance(data, (cls, ABCSeries)) and is_categorical_dtype(data): - data = data.values - - if not isinstance(data, ABCCategorical): - return Categorical(data, dtype=dtype) - - if isinstance(dtype, CategoricalDtype) and dtype != data.dtype: - # we want to silently ignore dtype='category' - data = data._set_dtype(dtype) - return data - - @classmethod - def _simple_new(cls, values, name=None, dtype=None): + def _simple_new(cls, values: Categorical, name: Label = None): + assert isinstance(values, Categorical), type(values) result = object.__new__(cls) - values = cls._create_categorical(values, dtype=dtype) result._data = values result.name = name @@ -268,10 +241,15 @@ def _simple_new(cls, values, name=None, dtype=None): # -------------------------------------------------------------------- @Appender(Index._shallow_copy.__doc__) - def _shallow_copy(self, values=None, dtype=None, **kwargs): - if dtype is None: - dtype = self.dtype - return super()._shallow_copy(values=values, dtype=dtype, **kwargs) + def _shallow_copy(self, values=None, name: Label = no_default): + name = self.name if name is no_default else name + + if values is None: + values = self.values + + cat = Categorical(values, dtype=self.dtype) + + return type(self)._simple_new(cat, name=name) def _is_dtype_compat(self, other) -> bool: """ @@ -295,7 +273,8 @@ def _is_dtype_compat(self, other) -> bool: values = other if not is_list_like(values): values = [values] - other = CategoricalIndex(self._create_categorical(other, dtype=self.dtype)) + cat = Categorical(other, dtype=self.dtype) + other = CategoricalIndex(cat) if not other.isin(values).all(): raise TypeError( "cannot append a non-category item to a CategoricalIndex" @@ -426,9 +405,9 @@ def unique(self, level=None): if level is not None: self._validate_index_level(level) result = self.values.unique() - # CategoricalIndex._shallow_copy keeps original dtype - # if not otherwise specified - return self._shallow_copy(result, dtype=result.dtype) + # Use _simple_new instead of _shallow_copy to ensure we keep dtype + # of result, not self. + return type(self)._simple_new(result, name=self.name) @Appender(Index.duplicated.__doc__) def duplicated(self, keep="first"): @@ -444,35 +423,6 @@ def _maybe_cast_indexer(self, key): code = self.codes.dtype.type(code) return code - def get_value(self, series: "Series", key: Any): - """ - Fast lookup of value from 1-dimensional ndarray. Only use this if you - know what you're doing - - Parameters - ---------- - series : Series - 1-dimensional array to take values from - key: : scalar - The value of this index at the position of the desired value, - otherwise the positional index of the desired value - - Returns - ------- - Any - The element of the series at the position indicated by the key - """ - k = key - try: - k = self._convert_scalar_indexer(k, kind="getitem") - indexer = self.get_loc(k) - return series.take([indexer])[0] - except (KeyError, TypeError): - pass - - # we might be a positional inexer - return Index.get_value(self, series, key) - @Appender(Index.where.__doc__) def where(self, cond, other=None): # TODO: Investigate an alternative implementation with @@ -483,7 +433,7 @@ def where(self, cond, other=None): other = self._na_value values = np.where(cond, self.values, other) cat = Categorical(values, dtype=self.dtype) - return self._shallow_copy(cat, **self._get_attributes_dict()) + return type(self)._simple_new(cat, name=self.name) def reindex(self, target, method=None, level=None, limit=None, tolerance=None): """ @@ -552,7 +502,8 @@ def reindex(self, target, method=None, level=None, limit=None, tolerance=None): return new_target, indexer def _reindex_non_unique(self, target): - """ reindex from a non-unique; which CategoricalIndex's are almost + """ + reindex from a non-unique; which CategoricalIndex's are almost always """ new_target, indexer = self.reindex(target) @@ -630,7 +581,7 @@ def _convert_scalar_indexer(self, key, kind: str): try: return self.categories._convert_scalar_indexer(key, kind="loc") except TypeError: - self._invalid_indexer("label", key) + raise KeyError(key) return super()._convert_scalar_indexer(key, kind=kind) @Appender(Index._convert_list_indexer.__doc__) @@ -813,6 +764,12 @@ def _delegate_method(self, name: str, *args, **kwargs): return res return CategoricalIndex(res, name=self.name) + def _wrap_joined_index( + self, joined: np.ndarray, other: "CategoricalIndex" + ) -> "CategoricalIndex": + name = get_op_result_name(self, other) + return self._create_from_codes(joined, name=name) + CategoricalIndex._add_numeric_methods_add_sub_disabled() CategoricalIndex._add_numeric_methods_disabled() diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index d06d0d499ef47..d3038ae88652b 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -1,13 +1,14 @@ """ Base and utility classes for tseries type pandas objects. """ +from datetime import datetime from typing import Any, List, Optional, Union import numpy as np from pandas._libs import NaT, iNaT, join as libjoin, lib -from pandas._libs.algos import unique_deltas from pandas._libs.tslibs import timezones +from pandas._typing import Label from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError from pandas.util._decorators import Appender, cache_readonly @@ -80,16 +81,7 @@ def wrapper(left, right): cache=True, ) @inherit_names( - [ - "__iter__", - "mean", - "freq", - "freqstr", - "_ndarray_values", - "asi8", - "_box_values", - "_box_func", - ], + ["mean", "freq", "freqstr", "asi8", "_box_values", "_box_func"], DatetimeLikeArrayMixin, ) class DatetimeIndexOpsMixin(ExtensionIndex): @@ -395,7 +387,6 @@ def _convert_scalar_indexer(self, key, kind: str): key : label of the slice bound kind : {'loc', 'getitem'} """ - assert kind in ["loc", "getitem"] if not is_scalar(key): @@ -406,12 +397,63 @@ def _convert_scalar_indexer(self, key, kind: str): is_int = is_integer(key) is_flt = is_float(key) if kind == "loc" and (is_int or is_flt): - self._invalid_indexer("label", key) + raise KeyError(key) elif kind == "getitem" and is_flt: - self._invalid_indexer("label", key) + raise KeyError(key) return super()._convert_scalar_indexer(key, kind=kind) + def _validate_partial_date_slice(self, reso: str): + raise NotImplementedError + + def _parsed_string_to_bounds(self, reso: str, parsed: datetime): + raise NotImplementedError + + def _partial_date_slice( + self, reso: str, parsed: datetime, use_lhs: bool = True, use_rhs: bool = True + ): + """ + Parameters + ---------- + reso : str + parsed : datetime + use_lhs : bool, default True + use_rhs : bool, default True + + Returns + ------- + slice or ndarray[intp] + """ + self._validate_partial_date_slice(reso) + + t1, t2 = self._parsed_string_to_bounds(reso, parsed) + i8vals = self.asi8 + unbox = self._data._unbox_scalar + + if self.is_monotonic: + + if len(self) and ( + (use_lhs and t1 < self[0] and t2 < self[0]) + or ((use_rhs and t1 > self[-1] and t2 > self[-1])) + ): + # we are out of range + raise KeyError + + # TODO: does this depend on being monotonic _increasing_? + + # a monotonic (sorted) series can be sliced + # Use asi8.searchsorted to avoid re-validating Periods/Timestamps + left = i8vals.searchsorted(unbox(t1), side="left") if use_lhs else None + right = i8vals.searchsorted(unbox(t2), side="right") if use_rhs else None + return slice(left, right) + + else: + lhs_mask = (i8vals >= unbox(t1)) if use_lhs else True + rhs_mask = (i8vals <= unbox(t2)) if use_rhs else True + + # try to find the dates + return (lhs_mask & rhs_mask).nonzero()[0] + # -------------------------------------------------------------------- __add__ = make_wrapped_arith_op("__add__") @@ -510,26 +552,6 @@ def _summary(self, name=None) -> str: result = result.replace("'", "") return result - def _concat_same_dtype(self, to_concat, name): - """ - Concatenate to_concat which has the same class. - """ - - # do not pass tz to set because tzlocal cannot be hashed - if len({str(x.dtype) for x in to_concat}) != 1: - raise ValueError("to_concat must have the same tz") - - new_data = type(self._data)._concat_same_type(to_concat) - - if not is_period_dtype(self.dtype): - # GH 3232: If the concat result is evenly spaced, we can retain the - # original frequency - is_diff_evenly_spaced = len(unique_deltas(new_data.asi8)) == 1 - if is_diff_evenly_spaced: - new_data._freq = self.freq - - return type(self)._simple_new(new_data, name=name) - def shift(self, periods=1, freq=None): """ Shift index by desired number of time frequency increments. @@ -582,7 +604,8 @@ def delete(self, loc): if loc.start in (0, None) or loc.stop in (len(self), None): freq = self.freq - return self._shallow_copy(new_i8s, freq=freq) + arr = type(self._data)._simple_new(new_i8s, dtype=self.dtype, freq=freq) + return type(self)._simple_new(arr, name=self.name) class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, Int64Index): @@ -619,18 +642,26 @@ def _set_freq(self, freq): self._data._freq = freq - def _shallow_copy(self, values=None, **kwargs): + def _shallow_copy(self, values=None, name: Label = lib.no_default): + name = self.name if name is lib.no_default else name + if values is None: values = self._data + if isinstance(values, type(self)): + values = values._data + if isinstance(values, np.ndarray): + # TODO: We would rather not get here + values = type(self._data)(values, dtype=self.dtype) + attributes = self._get_attributes_dict() - if "freq" not in kwargs and self.freq is not None: + if self.freq is not None: if isinstance(values, (DatetimeArray, TimedeltaArray)): if values.freq is None: del attributes["freq"] - attributes.update(kwargs) + attributes["name"] = name return type(self)._simple_new(values, **attributes) # -------------------------------------------------------------------- @@ -700,9 +731,7 @@ def intersection(self, other, sort=False): # this point, depending on the values. result._set_freq(None) - result = self._shallow_copy( - result._data, name=result.name, dtype=result.dtype, freq=None - ) + result = self._shallow_copy(result._data, name=result.name) if result.freq is None: result._set_freq("infer") return result @@ -801,7 +830,10 @@ def _union(self, other, sort): this, other = self._maybe_utc_convert(other) if this._can_fast_union(other): - return this._fast_union(other, sort=sort) + result = this._fast_union(other, sort=sort) + if result.freq is None: + result._set_freq("infer") + return result else: i8self = Int64Index._simple_new(self.asi8, name=self.name) i8other = Int64Index._simple_new(other.asi8, name=other.name) @@ -878,17 +910,14 @@ def _is_convertible_to_index_for_join(cls, other: Index) -> bool: return True return False - def _wrap_joined_index(self, joined, other): + def _wrap_joined_index(self, joined: np.ndarray, other): + assert other.dtype == self.dtype, (other.dtype, self.dtype) name = get_op_result_name(self, other) - if self._can_fast_union(other): - joined = self._shallow_copy(joined) - joined.name = name - return joined - else: - kwargs = {} - if hasattr(self, "tz"): - kwargs["tz"] = getattr(other, "tz", None) - return type(self)._simple_new(joined, name, **kwargs) + + freq = self.freq if self._can_fast_union(other) else None + new_data = type(self._data)._simple_new(joined, dtype=self.dtype, freq=freq) + + return type(self)._simple_new(new_data, name=name) # -------------------------------------------------------------------- # List-Like Methods @@ -896,12 +925,14 @@ def _wrap_joined_index(self, joined, other): def insert(self, loc, item): """ Make new Index inserting new item at location + Parameters ---------- loc : int item : object if not either a Python datetime or a numpy integer-like, returned Index dtype will be object rather than datetime. + Returns ------- new_index : Index @@ -934,7 +965,8 @@ def insert(self, loc, item): new_i8s = np.concatenate( (self[:loc].asi8, [item.view(np.int64)], self[loc:].asi8) ) - return self._shallow_copy(new_i8s, freq=freq) + arr = type(self._data)._simple_new(new_i8s, dtype=self.dtype, freq=freq) + return type(self)._simple_new(arr, name=self.name) except (AttributeError, TypeError): # fall back to object index diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 3d57f0944b318..e303e487b1a7d 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -503,19 +503,9 @@ def _parsed_string_to_bounds(self, reso: str, parsed: datetime): end = end.tz_localize(self.tz) return start, end - def _partial_date_slice( - self, reso: str, parsed: datetime, use_lhs: bool = True, use_rhs: bool = True - ): - """ - Parameters - ---------- - reso : str - use_lhs : bool, default True - use_rhs : bool, default True - """ - is_monotonic = self.is_monotonic + def _validate_partial_date_slice(self, reso: str): if ( - is_monotonic + self.is_monotonic and reso in ["day", "hour", "minute", "second"] and self._resolution >= Resolution.get_reso(reso) ): @@ -530,31 +520,6 @@ def _partial_date_slice( # _parsed_string_to_bounds allows it. raise KeyError - t1, t2 = self._parsed_string_to_bounds(reso, parsed) - stamps = self.asi8 - - if is_monotonic: - - # we are out of range - if len(stamps) and ( - (use_lhs and t1.value < stamps[0] and t2.value < stamps[0]) - or ((use_rhs and t1.value > stamps[-1] and t2.value > stamps[-1])) - ): - raise KeyError - - # a monotonic (sorted) series can be sliced - # Use asi8.searchsorted to avoid re-validating - left = stamps.searchsorted(t1.value, side="left") if use_lhs else None - right = stamps.searchsorted(t2.value, side="right") if use_rhs else None - - return slice(left, right) - - lhs_mask = (stamps >= t1.value) if use_lhs else True - rhs_mask = (stamps <= t2.value) if use_rhs else True - - # try to find a the dates - return (lhs_mask & rhs_mask).nonzero()[0] - def _maybe_promote(self, other): if other.inferred_type == "date": other = DatetimeIndex(other) @@ -974,7 +939,6 @@ def date_range( DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'], dtype='datetime64[ns]', freq='D') """ - if freq is None and com.any_none(periods, start, end): freq = "D" diff --git a/pandas/core/indexes/extension.py b/pandas/core/indexes/extension.py index 66b551f654bf1..daccb35864e98 100644 --- a/pandas/core/indexes/extension.py +++ b/pandas/core/indexes/extension.py @@ -39,7 +39,6 @@ def inherit_from_data(name: str, delegate, cache: bool = False, wrap: bool = Fal ------- attribute, method, property, or cache_readonly """ - attr = getattr(delegate, name) if isinstance(attr, property): @@ -196,6 +195,9 @@ class ExtensionIndex(Index): Index subclass for indexes backed by ExtensionArray. """ + # The base class already passes through to _data: + # size, __len__, dtype + _data: ExtensionArray __eq__ = _make_wrapped_comparison_op("__eq__") @@ -205,6 +207,9 @@ class ExtensionIndex(Index): __le__ = _make_wrapped_comparison_op("__le__") __ge__ = _make_wrapped_comparison_op("__ge__") + # --------------------------------------------------------------------- + # NDarray-Like Methods + def __getitem__(self, key): result = self._data[key] if isinstance(result, type(self._data)): @@ -217,6 +222,8 @@ def __getitem__(self, key): def __iter__(self): return self._data.__iter__() + # --------------------------------------------------------------------- + @property def _ndarray_values(self) -> np.ndarray: return self._data._ndarray_values @@ -235,6 +242,10 @@ def repeat(self, repeats, axis=None): result = self._data.repeat(repeats, axis=axis) return self._shallow_copy(result) + def _concat_same_dtype(self, to_concat, name): + arr = type(self._data)._concat_same_type(to_concat) + return type(self)._simple_new(arr, name=name) + @Appender(Index.take.__doc__) def take(self, indices, axis=0, allow_fill=True, fill_value=None, **kwargs): nv.validate_take(tuple(), kwargs) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 03fb8db2e1e1e..a7bb4237eab69 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -9,7 +9,7 @@ from pandas._libs import Timedelta, Timestamp, lib from pandas._libs.interval import Interval, IntervalMixin, IntervalTree -from pandas._typing import AnyArrayLike +from pandas._typing import AnyArrayLike, Label from pandas.util._decorators import Appender, Substitution, cache_readonly from pandas.util._exceptions import rewrite_exception @@ -183,28 +183,15 @@ def func(intvidx_self, other, sort=False): ) @inherit_names(["set_closed", "to_tuples"], IntervalArray, wrap=True) @inherit_names( - [ - "__len__", - "__array__", - "overlaps", - "contains", - "size", - "dtype", - "left", - "right", - "length", - ], - IntervalArray, + ["__array__", "overlaps", "contains", "left", "right", "length"], IntervalArray, ) @inherit_names( - ["is_non_overlapping_monotonic", "mid", "_ndarray_values", "closed"], - IntervalArray, - cache=True, + ["is_non_overlapping_monotonic", "mid", "closed"], IntervalArray, cache=True, ) class IntervalIndex(IntervalMixin, ExtensionIndex): _typ = "intervalindex" _comparables = ["name"] - _attributes = ["name", "closed"] + _attributes = ["name"] # we would like our indexing holder to defer to us _defer_to_indexing = True @@ -240,17 +227,15 @@ def __new__( return cls._simple_new(array, name) @classmethod - def _simple_new(cls, array, name, closed=None): + def _simple_new(cls, array: IntervalArray, name: Label = None): """ Construct from an IntervalArray Parameters ---------- array : IntervalArray - name : str + name : Label, default None Attached as result.name - closed : Any - Ignored. """ assert isinstance(array, IntervalArray), type(array) @@ -346,11 +331,12 @@ def from_tuples( # -------------------------------------------------------------------- @Appender(Index._shallow_copy.__doc__) - def _shallow_copy(self, left=None, right=None, **kwargs): - result = self._data._shallow_copy(left=left, right=right) + def _shallow_copy(self, values=None, **kwargs): + if values is None: + values = self._data attributes = self._get_attributes_dict() attributes.update(kwargs) - return self._simple_new(result, **attributes) + return self._simple_new(values, **attributes) @cache_readonly def _isnan(self): @@ -420,7 +406,7 @@ def astype(self, dtype, copy=True): with rewrite_exception("IntervalArray", type(self).__name__): new_values = self.values.astype(dtype, copy=copy) if is_interval_dtype(new_values): - return self._shallow_copy(new_values.left, new_values.right) + return self._shallow_copy(new_values) return Index.astype(self, dtype, copy=copy) @property @@ -563,7 +549,6 @@ def _can_reindex(self, indexer: np.ndarray) -> None: ------ ValueError if its a duplicate axis """ - # trying to reindex on an axis with duplicates if self.is_overlapping and len(indexer): raise ValueError("cannot reindex from an overlapping axis") @@ -885,7 +870,7 @@ def get_indexer_for(self, target: AnyArrayLike, **kwargs) -> np.ndarray: return self.get_indexer_non_unique(target)[0] return self.get_indexer(target, **kwargs) - def _convert_slice_indexer(self, key: slice, kind=None): + def _convert_slice_indexer(self, key: slice, kind: str): if not (key.step is None or key.step == 1): raise ValueError("cannot support not-default step in a slice") return super()._convert_slice_indexer(key, kind) @@ -895,7 +880,8 @@ def where(self, cond, other=None): if other is None: other = self._na_value values = np.where(cond, self.values, other) - return self._shallow_copy(values) + result = IntervalArray(values) + return self._shallow_copy(result) def delete(self, loc): """ @@ -907,7 +893,8 @@ def delete(self, loc): """ new_left = self.left.delete(loc) new_right = self.right.delete(loc) - return self._shallow_copy(new_left, new_right) + result = self._data._shallow_copy(new_left, new_right) + return self._shallow_copy(result) def insert(self, loc, item): """ @@ -941,19 +928,8 @@ def insert(self, loc, item): new_left = self.left.insert(loc, left_insert) new_right = self.right.insert(loc, right_insert) - return self._shallow_copy(new_left, new_right) - - def _concat_same_dtype(self, to_concat, name): - """ - assert that we all have the same .closed - we allow a 0-len index here as well - """ - if not len({i.closed for i in to_concat if len(i)}) == 1: - raise ValueError( - "can only append two IntervalIndex objects " - "that are closed on the same side" - ) - return super()._concat_same_dtype(to_concat, name) + result = self._data._shallow_copy(new_left, new_right) + return self._shallow_copy(result) @Appender(_index_shared_docs["take"] % _index_doc_kwargs) def take(self, indices, axis=0, allow_fill=True, fill_value=None, **kwargs): @@ -962,14 +938,6 @@ def take(self, indices, axis=0, allow_fill=True, fill_value=None, **kwargs): ) return self._shallow_copy(result) - def __getitem__(self, value): - result = self._data[value] - if isinstance(result, IntervalArray): - return self._shallow_copy(result) - else: - # scalar - return result - # -------------------------------------------------------------------- # Rendering Methods # __repr__ associated methods are based on MultiIndex diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 708bea7d132a2..4bd462e83a5bc 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -1,5 +1,15 @@ from sys import getsizeof -from typing import Any, Hashable, Iterable, List, Optional, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Hashable, + Iterable, + List, + Optional, + Sequence, + Tuple, + Union, +) import warnings import numpy as np @@ -48,7 +58,6 @@ indexer_from_factorized, lexsort_indexer, ) -from pandas.core.util.hashing import hash_tuple, hash_tuples from pandas.io.formats.printing import ( format_object_attrs, @@ -56,6 +65,9 @@ pprint_thing, ) +if TYPE_CHECKING: + from pandas import Series # noqa:F401 + _index_doc_kwargs = dict(ibase._index_doc_kwargs) _index_doc_kwargs.update( dict(klass="MultiIndex", target_klass="MultiIndex or list of tuples") @@ -126,7 +138,6 @@ def _codes_to_ints(self, codes): int, or 1-dimensional array of dtype object Integer(s) representing one combination (each). """ - # Shift the representation of each level by the pre-calculated number # of bits. Since this can overflow uint64, first make sure we are # working with Python integers: @@ -235,6 +246,7 @@ class MultiIndex(Index): rename = Index.set_names _tuples = None + sortorder: Optional[int] # -------------------------------------------------------------------- # Constructors @@ -1000,8 +1012,8 @@ def copy( levels=None, codes=None, deep=False, + name=None, _set_identity=False, - **kwargs, ): """ Make a copy of this object. Names, dtype, levels and codes can be @@ -1013,10 +1025,13 @@ def copy( dtype : numpy dtype or pandas type, optional levels : sequence, optional codes : sequence, optional + deep : bool, default False + name : Label + Kept for compatibility with 1-dimensional Index. Should not be used. Returns ------- - copy : MultiIndex + MultiIndex Notes ----- @@ -1024,10 +1039,7 @@ def copy( ``deep``, but if ``deep`` is passed it will attempt to deepcopy. This could be potentially expensive on large MultiIndex objects. """ - name = kwargs.get("name") names = self._validate_names(name=name, names=names, deep=deep) - if "labels" in kwargs: - raise TypeError("'labels' argument has been removed; use 'codes' instead") if deep: from copy import deepcopy @@ -1102,7 +1114,6 @@ def _nbytes(self, deep: bool = False) -> int: *this is in internal routine* """ - # for implementations with no useful getsizeof (PyPy) objsize = 24 @@ -1392,7 +1403,6 @@ def is_monotonic_increasing(self) -> bool: return if the index is monotonic increasing (only equal or increasing) values. """ - if all(x.is_monotonic for x in self.levels): # If each level is sorted, we can operate on the codes directly. GH27495 return libalgos.is_lexsorted( @@ -1420,56 +1430,11 @@ def is_monotonic_decreasing(self) -> bool: # monotonic decreasing if and only if reverse is monotonic increasing return self[::-1].is_monotonic_increasing - @cache_readonly - def _have_mixed_levels(self): - """ return a boolean list indicated if we have mixed levels """ - return ["mixed" in l for l in self._inferred_type_levels] - @cache_readonly def _inferred_type_levels(self): """ return a list of the inferred types, one for each level """ return [i.inferred_type for i in self.levels] - @cache_readonly - def _hashed_values(self): - """ return a uint64 ndarray of my hashed values """ - return hash_tuples(self) - - def _hashed_indexing_key(self, key): - """ - validate and return the hash for the provided key - - *this is internal for use for the cython routines* - - Parameters - ---------- - key : string or tuple - - Returns - ------- - np.uint64 - - Notes - ----- - we need to stringify if we have mixed levels - """ - - if not isinstance(key, tuple): - return hash_tuples(key) - - if not len(key) == self.nlevels: - raise KeyError - - def f(k, stringify): - if stringify and not isinstance(k, str): - k = str(k) - return k - - key = tuple( - f(k, stringify) for k, stringify in zip(key, self._have_mixed_levels) - ) - return hash_tuple(key) - @Appender(Index.duplicated.__doc__) def duplicated(self, keep="first"): shape = map(len, self.levels) @@ -1513,7 +1478,6 @@ def _get_level_values(self, level, unique=False): ------- values : ndarray """ - lev = self.levels[level] level_codes = self.codes[level] name = self._names[level] @@ -1541,7 +1505,6 @@ def get_level_values(self, level): Examples -------- - Create a MultiIndex: >>> mi = pd.MultiIndex.from_arrays((list('abc'), list('def'))) @@ -1585,7 +1548,7 @@ def to_frame(self, index=True, name=None): index : bool, default True Set the index of the returned DataFrame as the original MultiIndex. - name : list / sequence of strings, optional + name : list / sequence of str, optional The passed names should substitute index level names. Returns @@ -1596,7 +1559,6 @@ def to_frame(self, index=True, name=None): -------- DataFrame """ - from pandas import DataFrame if name is not None: @@ -1679,7 +1641,7 @@ def _lexsort_depth(self) -> int: MultiIndex that are sorted lexically Returns - ------ + ------- int """ int64_codes = [ensure_int64(level_codes) for level_codes in self.codes] @@ -1706,7 +1668,6 @@ def _sort_levels_monotonic(self): Examples -------- - >>> mi = pd.MultiIndex(levels=[['a', 'b'], ['bb', 'aa']], ... codes=[[0, 0, 1, 1], [0, 1, 0, 1]]) >>> mi @@ -1723,7 +1684,6 @@ def _sort_levels_monotonic(self): ('b', 'bb')], ) """ - if self.is_lexsorted() and self.is_monotonic: return self @@ -1792,7 +1752,6 @@ def remove_unused_levels(self): >>> mi2.levels FrozenList([[1], ['a', 'b']]) """ - new_levels = [] new_codes = [] @@ -1855,28 +1814,6 @@ def __reduce__(self): ) return ibase._new_Index, (type(self), d), None - def __setstate__(self, state): - """Necessary for making this object picklable""" - - if isinstance(state, dict): - levels = state.get("levels") - codes = state.get("codes") - sortorder = state.get("sortorder") - names = state.get("names") - - elif isinstance(state, tuple): - - nd_state, own_state = state - levels, codes, sortorder, names = own_state - - self._set_levels([Index(x) for x in levels], validate=False) - self._set_codes(codes) - new_codes = self._verify_integrity() - self._set_codes(new_codes) - self._set_names(names) - self.sortorder = sortorder - self._reset_identity() - # -------------------------------------------------------------------- def __getitem__(self, key): @@ -2136,6 +2073,9 @@ def reorder_levels(self, order): Parameters ---------- + order : list of int or list of str + List representing new level order. Reference level by number + (position) or by key (label). Returns ------- @@ -2326,28 +2266,32 @@ def get_value(self, series, key): # We have to explicitly exclude generators, as these are hashable. raise InvalidIndexError(key) - def _try_mi(k): - # TODO: what if a level contains tuples?? - loc = self.get_loc(k) - - new_values = series._values[loc] - if is_scalar(loc): - return new_values - - new_index = self[loc] - new_index = maybe_droplevels(new_index, k) - return series._constructor( - new_values, index=new_index, name=series.name - ).__finalize__(self) - try: - return _try_mi(key) + loc = self.get_loc(key) except KeyError: if is_integer(key): - return series._values[key] + loc = key else: raise + return self._get_values_for_loc(series, loc, key) + + def _get_values_for_loc(self, series: "Series", loc, key): + """ + Do a positional lookup on the given Series, returning either a scalar + or a Series. + + Assumes that `series.index is self` + """ + new_values = series._values[loc] + if is_scalar(loc): + return new_values + + new_index = self[loc] + new_index = maybe_droplevels(new_index, key) + new_ser = series._constructor(new_values, index=new_index, name=series.name) + return new_ser.__finalize__(series) + def _convert_listlike_indexer(self, keyarr): """ Parameters @@ -2379,6 +2323,35 @@ def _convert_listlike_indexer(self, keyarr): return indexer, keyarr + def _get_partial_string_timestamp_match_key(self, key): + """ + Translate any partial string timestamp matches in key, returning the + new key. + + Only relevant for MultiIndex. + """ + # GH#10331 + if isinstance(key, str) and self.levels[0]._supports_partial_string_indexing: + # Convert key '2016-01-01' to + # ('2016-01-01'[, slice(None, None, None)]+) + key = tuple([key] + [slice(None)] * (len(self.levels) - 1)) + + if isinstance(key, tuple): + # Convert (..., '2016-01-01', ...) in tuple to + # (..., slice('2016-01-01', '2016-01-01', None), ...) + new_key = [] + for i, component in enumerate(key): + if ( + isinstance(component, str) + and self.levels[i]._supports_partial_string_indexing + ): + new_key.append(slice(component, component, None)) + else: + new_key.append(component) + key = tuple(new_key) + + return key + @Appender(_index_shared_docs["get_indexer"] % _index_doc_kwargs) def get_indexer(self, target, method=None, limit=None, tolerance=None): method = missing.clean_reindex_fill_method(method) @@ -2469,7 +2442,6 @@ def get_slice_bound( MultiIndex.get_locs : Get location for a label/slice/list/mask or a sequence of such. """ - if not isinstance(label, tuple): label = (label,) return self._partial_tup_index(label, side=side) @@ -2579,7 +2551,6 @@ def _get_loc_single_level_index(self, level_index: Index, key: Hashable) -> int: -------- Index.get_loc : The get_loc method for (single-level) index. """ - if is_scalar(key) and isna(key): return -1 else: @@ -2734,7 +2705,6 @@ def get_loc_level(self, key, level=0, drop_level: bool = True): >>> mi.get_loc_level(['b', 'e']) (1, None) """ - # different name to distinguish from maybe_droplevels def maybe_mi_droplevels(indexer, levels, drop_level: bool): if not drop_level: @@ -3297,9 +3267,23 @@ def intersection(self, other, sort=False): if self.equals(other): return self - self_tuples = self._ndarray_values - other_tuples = other._ndarray_values - uniq_tuples = set(self_tuples) & set(other_tuples) + lvals = self._ndarray_values + rvals = other._ndarray_values + + uniq_tuples = None # flag whether _inner_indexer was succesful + if self.is_monotonic and other.is_monotonic: + try: + uniq_tuples = self._inner_indexer(lvals, rvals)[0] + sort = False # uniq_tuples is already sorted + except TypeError: + pass + + if uniq_tuples is None: + other_uniq = set(rvals) + seen = set() + uniq_tuples = [ + x for x in lvals if x in other_uniq and not (x in seen or seen.add(x)) + ] if sort is None: uniq_tuples = sorted(uniq_tuples) diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index d67c40a78d807..06a26cc90555e 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -3,7 +3,7 @@ import numpy as np from pandas._libs import index as libindex, lib -from pandas._typing import Dtype +from pandas._typing import Dtype, Label from pandas.util._decorators import Appender, cache_readonly from pandas.core.dtypes.cast import astype_nansafe @@ -103,11 +103,16 @@ def _maybe_cast_slice_bound(self, label, side, kind): return self._maybe_cast_indexer(label) @Appender(Index._shallow_copy.__doc__) - def _shallow_copy(self, values=None, **kwargs): - if values is not None and not self._can_hold_na: + def _shallow_copy(self, values=None, name: Label = lib.no_default): + name = name if name is not lib.no_default else self.name + + if values is not None and not self._can_hold_na and values.dtype.kind == "f": # Ensure we are not returning an Int64Index with float data: - return self._shallow_copy_with_infer(values=values, **kwargs) - return super()._shallow_copy(values=values, **kwargs) + return Float64Index._simple_new(values, name=name) + + if values is None: + values = self.values + return type(self)._simple_new(values, name=name) def _convert_for_op(self, value): """ @@ -393,11 +398,10 @@ def _convert_scalar_indexer(self, key, kind: str): return key @Appender(Index._convert_slice_indexer.__doc__) - def _convert_slice_indexer(self, key: slice, kind=None): - - if kind == "iloc": - return super()._convert_slice_indexer(key, kind=kind) + def _convert_slice_indexer(self, key: slice, kind: str): + assert kind in ["loc", "getitem"] + # We always treat __getitem__ slicing as label-based # translate to locations return self.slice_indexer(key.start, key.stop, key.step, kind=kind) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 42f0a012902a3..35a5d99abf4e6 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -5,9 +5,11 @@ import numpy as np from pandas._libs import index as libindex +from pandas._libs.lib import no_default from pandas._libs.tslibs import frequencies as libfrequencies, resolution from pandas._libs.tslibs.parsing import parse_time_string from pandas._libs.tslibs.period import Period +from pandas._typing import Label from pandas.util._decorators import Appender, cache_readonly from pandas.core.dtypes.common import ( @@ -85,7 +87,7 @@ class PeriodIndex(DatetimeIndexOpsMixin, Int64Index): copy : bool Make a copy of input ndarray. freq : str or period object, optional - One of pandas period strings or corresponding objects + One of pandas period strings or corresponding objects. year : int, array, or Series, default None month : int, array, or Series, default None quarter : int, array, or Series, default None @@ -215,7 +217,7 @@ def __new__( return cls._simple_new(data, name=name) @classmethod - def _simple_new(cls, values, name=None, freq=None, **kwargs): + def _simple_new(cls, values: PeriodArray, name: Label = None): """ Create a new PeriodIndex. @@ -226,7 +228,6 @@ def _simple_new(cls, values, name=None, freq=None, **kwargs): or coercion. """ assert isinstance(values, PeriodArray), type(values) - assert freq is None or freq == values.freq, (freq, values.freq) result = object.__new__(cls) result._data = values @@ -248,8 +249,10 @@ def _has_complex_internals(self): # used to avoid libreduction code paths, which raise or require conversion return True - def _shallow_copy(self, values=None, **kwargs): + def _shallow_copy(self, values=None, name: Label = no_default): # TODO: simplify, figure out type of values + name = name if name is not no_default else self.name + if values is None: values = self._data @@ -263,18 +266,7 @@ def _shallow_copy(self, values=None, **kwargs): # GH#30713 this should never be reached raise TypeError(type(values), getattr(values, "dtype", None)) - # We don't allow changing `freq` in _shallow_copy. - validate_dtype_freq(self.dtype, kwargs.get("freq")) - attributes = self._get_attributes_dict() - - attributes.update(kwargs) - if not len(values) and "dtype" not in kwargs: - attributes["dtype"] = self.dtype - return self._simple_new(values, **attributes) - - def _shallow_copy_with_infer(self, values=None, **kwargs): - """ we always want to return a PeriodIndex """ - return self._shallow_copy(values=values, **kwargs) + return self._simple_new(values, name=name) def _maybe_convert_timedelta(self, other): """ @@ -468,6 +460,10 @@ def get_indexer(self, target, method=None, limit=None, tolerance=None): if tolerance is not None: tolerance = self._convert_tolerance(tolerance, target) + if self_index is not self: + # convert tolerance to i8 + tolerance = self._maybe_convert_timedelta(tolerance) + return Index.get_indexer(self_index, target, method, limit, tolerance) @Appender(_index_shared_docs["get_indexer_non_unique"] % _index_doc_kwargs) @@ -504,6 +500,7 @@ def get_loc(self, key, method=None, tolerance=None): TypeError If key is listlike or otherwise not hashable. """ + orig_key = key if not is_scalar(key): raise InvalidIndexError(key) @@ -545,20 +542,12 @@ def get_loc(self, key, method=None, tolerance=None): key = Period(key, freq=self.freq) except ValueError: # we cannot construct the Period - raise KeyError(key) + raise KeyError(orig_key) - ordinal = self._data._unbox_scalar(key) try: - return self._engine.get_loc(ordinal) + return Index.get_loc(self, key, method, tolerance) except KeyError: - - try: - if tolerance is not None: - tolerance = self._convert_tolerance(tolerance, np.asarray(key)) - return self._int64index.get_loc(ordinal, method, tolerance) - - except KeyError: - raise KeyError(key) + raise KeyError(orig_key) def _maybe_cast_slice_bound(self, label, side: str, kind: str): """ @@ -606,9 +595,7 @@ def _parsed_string_to_bounds(self, reso: str, parsed: datetime): iv = Period(parsed, freq=(grp, 1)) return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end")) - def _get_string_slice(self, key: str, use_lhs: bool = True, use_rhs: bool = True): - # TODO: Check for non-True use_lhs/use_rhs - parsed, reso = parse_time_string(key, self.freq) + def _validate_partial_date_slice(self, reso: str): grp = resolution.Resolution.get_freq_group(reso) freqn = resolution.get_freq_group(self.freq) @@ -616,41 +603,16 @@ def _get_string_slice(self, key: str, use_lhs: bool = True, use_rhs: bool = True # TODO: we used to also check for # reso in ["day", "hour", "minute", "second"] # why is that check not needed? - raise ValueError(key) - - t1, t2 = self._parsed_string_to_bounds(reso, parsed) - i8vals = self.asi8 + raise ValueError - if self.is_monotonic: - - # we are out of range - if len(self) and ( - (use_lhs and t1 < self[0] and t2 < self[0]) - or ((use_rhs and t1 > self[-1] and t2 > self[-1])) - ): - raise KeyError(key) - - # TODO: does this depend on being monotonic _increasing_? - # If so, DTI will also be affected. - - # a monotonic (sorted) series can be sliced - # Use asi8.searchsorted to avoid re-validating Periods - left = i8vals.searchsorted(t1.ordinal, side="left") if use_lhs else None - right = i8vals.searchsorted(t2.ordinal, side="right") if use_rhs else None - return slice(left, right) - - else: - lhs_mask = (i8vals >= t1.ordinal) if use_lhs else True - rhs_mask = (i8vals <= t2.ordinal) if use_rhs else True - - # try to find a the dates - return (lhs_mask & rhs_mask).nonzero()[0] + def _get_string_slice(self, key: str, use_lhs: bool = True, use_rhs: bool = True): + # TODO: Check for non-True use_lhs/use_rhs + parsed, reso = parse_time_string(key, self.freq) - def _convert_tolerance(self, tolerance, target): - tolerance = DatetimeIndexOpsMixin._convert_tolerance(self, tolerance, target) - if target.size != tolerance.size and tolerance.size > 1: - raise ValueError("list-like tolerance size must match target index size") - return self._maybe_convert_timedelta(tolerance) + try: + return self._partial_date_slice(reso, parsed, use_lhs, use_rhs) + except KeyError: + raise KeyError(key) def insert(self, loc, item): if not isinstance(item, Period) or self.freq != item.freq: @@ -697,12 +659,28 @@ def _assert_can_do_setop(self, other): if isinstance(other, PeriodIndex) and self.freq != other.freq: raise raise_on_incompatible(self, other) - def intersection(self, other, sort=False): + def _setop(self, other, sort, opname: str): + """ + Perform a set operation by dispatching to the Int64Index implementation. + """ self._validate_sort_keyword(sort) self._assert_can_do_setop(other) res_name = get_op_result_name(self, other) other = ensure_index(other) + i8self = Int64Index._simple_new(self.asi8) + i8other = Int64Index._simple_new(other.asi8) + i8result = getattr(i8self, opname)(i8other, sort=sort) + + parr = type(self._data)(np.asarray(i8result, dtype=np.int64), dtype=self.dtype) + result = type(self)._simple_new(parr, name=res_name) + return result + + def intersection(self, other, sort=False): + self._validate_sort_keyword(sort) + self._assert_can_do_setop(other) + other = ensure_index(other) + if self.equals(other): return self._get_reconciled_name_object(other) @@ -712,22 +690,16 @@ def intersection(self, other, sort=False): other = other.astype("O") return this.intersection(other, sort=sort) - i8self = Int64Index._simple_new(self.asi8) - i8other = Int64Index._simple_new(other.asi8) - i8result = i8self.intersection(i8other, sort=sort) - - result = self._shallow_copy(np.asarray(i8result, dtype=np.int64), name=res_name) - return result + return self._setop(other, sort, opname="intersection") def difference(self, other, sort=None): self._validate_sort_keyword(sort) self._assert_can_do_setop(other) - res_name = get_op_result_name(self, other) other = ensure_index(other) if self.equals(other): # pass an empty PeriodArray with the appropriate dtype - return self._shallow_copy(self._data[:0]) + return type(self)._simple_new(self._data[:0], name=self.name) if is_object_dtype(other): return self.astype(object).difference(other).astype(self.dtype) @@ -735,12 +707,7 @@ def difference(self, other, sort=None): elif not is_dtype_equal(self.dtype, other.dtype): return self - i8self = Int64Index._simple_new(self.asi8) - i8other = Int64Index._simple_new(other.asi8) - i8result = i8self.difference(i8other, sort=sort) - - result = self._shallow_copy(np.asarray(i8result, dtype=np.int64), name=res_name) - return result + return self._setop(other, sort, opname="difference") def _union(self, other, sort): if not len(other) or self.equals(other) or not len(self): @@ -754,13 +721,7 @@ def _union(self, other, sort): other = other.astype("O") return this._union(other, sort=sort) - i8self = Int64Index._simple_new(self.asi8) - i8other = Int64Index._simple_new(other.asi8) - i8result = i8self._union(i8other, sort=sort) - - res_name = get_op_result_name(self, other) - result = self._shallow_copy(np.asarray(i8result, dtype=np.int64), name=res_name) - return result + return self._setop(other, sort, opname="_union") # ------------------------------------------------------------------------ @@ -819,7 +780,6 @@ def period_range( Examples -------- - >>> pd.period_range(start='2017-01-01', end='2018-01-01', freq='M') PeriodIndex(['2017-01', '2017-02', '2017-03', '2017-04', '2017-05', '2017-06', '2017-06', '2017-07', '2017-08', '2017-09', diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index d6752da6bc58f..71cc62e6a110b 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -7,6 +7,8 @@ import numpy as np from pandas._libs import index as libindex +from pandas._libs.lib import no_default +from pandas._typing import Label import pandas.compat as compat from pandas.compat.numpy import function as nv from pandas.util._decorators import Appender, cache_readonly @@ -93,7 +95,7 @@ def __new__( # RangeIndex if isinstance(start, RangeIndex): start = start._range - return cls._simple_new(start, dtype=dtype, name=name) + return cls._simple_new(start, name=name) # validate the arguments if com.all_none(start, stop, step): @@ -111,7 +113,7 @@ def __new__( raise ValueError("Step must not be zero") rng = range(start, stop, step) - return cls._simple_new(rng, dtype=dtype, name=name) + return cls._simple_new(rng, name=name) @classmethod def from_range(cls, data: range, name=None, dtype=None) -> "RangeIndex": @@ -129,10 +131,10 @@ def from_range(cls, data: range, name=None, dtype=None) -> "RangeIndex": ) cls._validate_dtype(dtype) - return cls._simple_new(data, dtype=dtype, name=name) + return cls._simple_new(data, name=name) @classmethod - def _simple_new(cls, values: range, name=None, dtype=None) -> "RangeIndex": + def _simple_new(cls, values: range, name: Label = None) -> "RangeIndex": result = object.__new__(cls) assert isinstance(values, range) @@ -385,13 +387,13 @@ def tolist(self): return list(self._range) @Appender(Int64Index._shallow_copy.__doc__) - def _shallow_copy(self, values=None, **kwargs): + def _shallow_copy(self, values=None, name: Label = no_default): + name = self.name if name is no_default else name + if values is None: - name = kwargs.get("name", self.name) return self._simple_new(self._range, name=name) else: - kwargs.setdefault("name", self.name) - return self._int64index._shallow_copy(values, **kwargs) + return Int64Index._simple_new(values, name=name) @Appender(Int64Index.copy.__doc__) def copy(self, name=None, deep=False, dtype=None, **kwargs): diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index ec0414adc1376..b3b2bc46f6659 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -173,22 +173,15 @@ def __new__( def _simple_new(cls, values, name=None, freq=None, dtype=_TD_DTYPE): # `dtype` is passed by _shallow_copy in corner cases, should always # be timedelta64[ns] if present - - if not isinstance(values, TimedeltaArray): - values = TimedeltaArray._simple_new(values, dtype=dtype, freq=freq) - else: - if freq is None: - freq = values.freq - assert isinstance(values, TimedeltaArray), type(values) assert dtype == _TD_DTYPE, dtype - assert values.dtype == "m8[ns]", values.dtype + assert isinstance(values, TimedeltaArray) + assert freq is None or values.freq == freq - tdarr = TimedeltaArray._simple_new(values._data, freq=freq) result = object.__new__(cls) - result._data = tdarr + result._data = values result._name = name # For groupby perf. See note in indexes/base about _index_data - result._index_data = tdarr._data + result._index_data = values._data result._reset_identity() return result @@ -328,7 +321,6 @@ def timedelta_range( Examples -------- - >>> pd.timedelta_range(start='1 day', periods=4) TimedeltaIndex(['1 days', '2 days', '3 days', '4 days'], dtype='timedelta64[ns]', freq='D') diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 5c0f893554957..a0e96ac169ff7 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -8,11 +8,11 @@ from pandas.util._decorators import Appender from pandas.core.dtypes.common import ( - is_float, is_integer, is_iterator, is_list_like, is_numeric_dtype, + is_object_dtype, is_scalar, is_sequence, ) @@ -26,8 +26,7 @@ is_list_like_indexer, length_of_indexer, ) -from pandas.core.indexes.api import Index -from pandas.core.indexes.base import InvalidIndexError +from pandas.core.indexes.api import Index, InvalidIndexError # "null slice" _NS = slice(None, None) @@ -49,7 +48,6 @@ class _IndexSlice: Examples -------- - >>> midx = pd.MultiIndex.from_product([['A0','A1'], ['B0','B1','B2','B3']]) >>> columns = ['foo', 'bar'] >>> dfmi = pd.DataFrame(np.arange(16).reshape((len(midx), len(columns))), @@ -87,7 +85,8 @@ class IndexingError(Exception): class IndexingMixin: - """Mixin for adding .loc/.iloc/.at/.iat to Datafames and Series. + """ + Mixin for adding .loc/.iloc/.at/.iat to Datafames and Series. """ @property @@ -125,7 +124,6 @@ def iloc(self) -> "_iLocIndexer": Examples -------- - >>> mydict = [{'a': 1, 'b': 2, 'c': 3, 'd': 4}, ... {'a': 100, 'b': 200, 'c': 300, 'd': 400}, ... {'a': 1000, 'b': 2000, 'c': 3000, 'd': 4000 }] @@ -579,19 +577,10 @@ def __call__(self, axis=None): new_self.axis = axis return new_self - def _get_label(self, label, axis: int): - if self.ndim == 1: - # for perf reasons we want to try _xs first - # as its basically direct indexing - # but will fail when the index is not present - # see GH5667 - return self.obj._xs(label, axis=axis) - elif isinstance(label, tuple) and isinstance(label[axis], slice): - raise IndexingError("no slices here, handle elsewhere") - - return self.obj._xs(label, axis=axis) - def _get_setitem_indexer(self, key): + """ + Convert a potentially-label-based key into a positional indexer. + """ if self.axis is not None: return self._convert_tuple(key, is_setter=True) @@ -628,7 +617,10 @@ def __setitem__(self, key, value): else: key = com.apply_if_callable(key, self.obj) indexer = self._get_setitem_indexer(key) - self._setitem_with_indexer(indexer, value) + self._has_valid_setitem_indexer(key) + + iloc = self if self.name == "iloc" else self.obj.iloc + iloc._setitem_with_indexer(indexer, value) def _validate_key(self, key, axis: int): """ @@ -696,493 +688,333 @@ def _convert_tuple(self, key, is_setter: bool = False): keyidx.append(idx) return tuple(keyidx) - def _setitem_with_indexer(self, indexer, value): - self._has_valid_setitem_indexer(indexer) - - # also has the side effect of consolidating in-place - from pandas import Series + def _getitem_tuple_same_dim(self, tup: Tuple): + """ + Index with indexers that should return an object of the same dimension + as self.obj. - info_axis = self.obj._info_axis_number + This is only called after a failed call to _getitem_lowerdim. + """ + retval = self.obj + for i, key in enumerate(tup): + if com.is_null_slice(key): + continue - # maybe partial set - take_split_path = self.obj._is_mixed_type + retval = getattr(retval, self.name)._getitem_axis(key, axis=i) + # We should never have retval.ndim < self.ndim, as that should + # be handled by the _getitem_lowerdim call above. + assert retval.ndim == self.ndim - # if there is only one block/type, still have to take split path - # unless the block is one-dimensional or it can hold the value - if not take_split_path and self.obj._data.blocks: - (blk,) = self.obj._data.blocks - if 1 < blk.ndim: # in case of dict, keys are indices - val = list(value.values()) if isinstance(value, dict) else value - take_split_path = not blk._can_hold_element(val) + return retval - # if we have any multi-indexes that have non-trivial slices - # (not null slices) then we must take the split path, xref - # GH 10360, GH 27841 - if isinstance(indexer, tuple) and len(indexer) == len(self.obj.axes): - for i, ax in zip(indexer, self.obj.axes): - if isinstance(ax, ABCMultiIndex) and not ( - is_integer(i) or com.is_null_slice(i) - ): - take_split_path = True - break + def _getitem_lowerdim(self, tup: Tuple): - if isinstance(indexer, tuple): - nindexer = [] - for i, idx in enumerate(indexer): - if isinstance(idx, dict): + # we can directly get the axis result since the axis is specified + if self.axis is not None: + axis = self.obj._get_axis_number(self.axis) + return self._getitem_axis(tup, axis=axis) - # reindex the axis to the new value - # and set inplace - key, _ = convert_missing_indexer(idx) + # we may have a nested tuples indexer here + if self._is_nested_tuple_indexer(tup): + return self._getitem_nested_tuple(tup) - # if this is the items axes, then take the main missing - # path first - # this correctly sets the dtype and avoids cache issues - # essentially this separates out the block that is needed - # to possibly be modified - if self.ndim > 1 and i == self.obj._info_axis_number: + # we maybe be using a tuple to represent multiple dimensions here + ax0 = self.obj._get_axis(0) + # ...but iloc should handle the tuple as simple integer-location + # instead of checking it as multiindex representation (GH 13797) + if isinstance(ax0, ABCMultiIndex) and self.name != "iloc": + result = self._handle_lowerdim_multi_index_axis0(tup) + if result is not None: + return result - # add the new item, and set the value - # must have all defined axes if we have a scalar - # or a list-like on the non-info axes if we have a - # list-like - len_non_info_axes = ( - len(_ax) for _i, _ax in enumerate(self.obj.axes) if _i != i - ) - if any(not l for l in len_non_info_axes): - if not is_list_like_indexer(value): - raise ValueError( - "cannot set a frame with no " - "defined index and a scalar" - ) - self.obj[key] = value - return self.obj + if len(tup) > self.ndim: + raise IndexingError("Too many indexers. handle elsewhere") - # add a new item with the dtype setup - self.obj[key] = _infer_fill_value(value) + for i, key in enumerate(tup): + if is_label_like(key): + # We don't need to check for tuples here because those are + # caught by the _is_nested_tuple_indexer check above. + section = self._getitem_axis(key, axis=i) - new_indexer = convert_from_missing_indexer_tuple( - indexer, self.obj.axes - ) - self._setitem_with_indexer(new_indexer, value) + # We should never have a scalar section here, because + # _getitem_lowerdim is only called after a check for + # is_scalar_access, which that would be. + if section.ndim == self.ndim: + # we're in the middle of slicing through a MultiIndex + # revise the key wrt to `section` by inserting an _NS + new_key = tup[:i] + (_NS,) + tup[i + 1 :] - return self.obj + else: + # Note: the section.ndim == self.ndim check above + # rules out having DataFrame here, so we dont need to worry + # about transposing. + new_key = tup[:i] + tup[i + 1 :] - # reindex the axis - # make sure to clear the cache because we are - # just replacing the block manager here - # so the object is the same - index = self.obj._get_axis(i) - labels = index.insert(len(index), key) - self.obj._data = self.obj.reindex(labels, axis=i)._data - self.obj._maybe_update_cacher(clear=True) - self.obj._is_copy = None + if len(new_key) == 1: + new_key = new_key[0] - nindexer.append(labels.get_loc(key)) + # Slices should return views, but calling iloc/loc with a null + # slice returns a new object. + if com.is_null_slice(new_key): + return section + # This is an elided recursive call to iloc/loc + return getattr(section, self.name)[new_key] - else: - nindexer.append(idx) + raise IndexingError("not applicable") - indexer = tuple(nindexer) - else: + def _getitem_nested_tuple(self, tup: Tuple): + # we have a nested tuple so have at least 1 multi-index level + # we should be able to match up the dimensionality here - indexer, missing = convert_missing_indexer(indexer) + # we have too many indexers for our dim, but have at least 1 + # multi-index dimension, try to see if we have something like + # a tuple passed to a series with a multi-index + if len(tup) > self.ndim: + if self.name != "loc": + # This should never be reached, but lets be explicit about it + raise ValueError("Too many indices") + result = self._handle_lowerdim_multi_index_axis0(tup) + if result is not None: + return result - if missing: - return self._setitem_with_indexer_missing(indexer, value) + # this is a series with a multi-index specified a tuple of + # selectors + axis = self.axis or 0 + return self._getitem_axis(tup, axis=axis) - # set - item_labels = self.obj._get_axis(info_axis) + # handle the multi-axis by taking sections and reducing + # this is iterative + obj = self.obj + axis = 0 + for i, key in enumerate(tup): - # align and set the values - if take_split_path: - # Above we only set take_split_path to True for 2D cases - assert self.ndim == 2 - assert info_axis == 1 + if com.is_null_slice(key): + axis += 1 + continue - if not isinstance(indexer, tuple): - indexer = _tuplify(self.ndim, indexer) + current_ndim = obj.ndim + obj = getattr(obj, self.name)._getitem_axis(key, axis=axis) + axis += 1 - if isinstance(value, ABCSeries): - value = self._align_series(indexer, value) + # if we have a scalar, we are done + if is_scalar(obj) or not hasattr(obj, "ndim"): + break - info_idx = indexer[info_axis] - if is_integer(info_idx): - info_idx = [info_idx] - labels = item_labels[info_idx] + # has the dim of the obj changed? + # GH 7199 + if obj.ndim < current_ndim: + axis -= 1 - # if we have a partial multiindex, then need to adjust the plane - # indexer here - if len(labels) == 1 and isinstance( - self.obj[labels[0]].axes[0], ABCMultiIndex - ): - item = labels[0] - obj = self.obj[item] - index = obj.index - idx = indexer[:info_axis][0] + return obj - plane_indexer = tuple([idx]) + indexer[info_axis + 1 :] - lplane_indexer = length_of_indexer(plane_indexer[0], index) + def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): + raise AbstractMethodError(self) - # require that we are setting the right number of values that - # we are indexing - if ( - is_list_like_indexer(value) - and np.iterable(value) - and lplane_indexer != len(value) - ): + def __getitem__(self, key): + if type(key) is tuple: + key = tuple(com.apply_if_callable(x, self.obj) for x in key) + if self._is_scalar_access(key): + try: + return self.obj._get_value(*key, takeable=self._takeable) + except (KeyError, IndexError, AttributeError): + # AttributeError for IntervalTree get_value + pass + return self._getitem_tuple(key) + else: + # we by definition only have the 0th axis + axis = self.axis or 0 - if len(obj[idx]) != len(value): - raise ValueError( - "cannot set using a multi-index " - "selection indexer with a different " - "length than the value" - ) + maybe_callable = com.apply_if_callable(key, self.obj) + return self._getitem_axis(maybe_callable, axis=axis) - # make sure we have an ndarray - value = getattr(value, "values", value).ravel() - - # we can directly set the series here - # as we select a slice indexer on the mi - if isinstance(idx, slice): - idx = index._convert_slice_indexer(idx) - obj._consolidate_inplace() - obj = obj.copy() - obj._data = obj._data.setitem(indexer=tuple([idx]), value=value) - self.obj[item] = obj - return + def _is_scalar_access(self, key: Tuple): + raise NotImplementedError() - # non-mi - else: - plane_indexer = indexer[:info_axis] + indexer[info_axis + 1 :] - plane_axis = self.obj.axes[:info_axis][0] - lplane_indexer = length_of_indexer(plane_indexer[0], plane_axis) + def _getitem_tuple(self, tup: Tuple): + raise AbstractMethodError(self) - def setter(item, v): - s = self.obj[item] - pi = plane_indexer[0] if lplane_indexer == 1 else plane_indexer + def _getitem_axis(self, key, axis: int): + raise NotImplementedError() - # perform the equivalent of a setitem on the info axis - # as we have a null slice or a slice with full bounds - # which means essentially reassign to the columns of a - # multi-dim object - # GH6149 (null slice), GH10408 (full bounds) - if isinstance(pi, tuple) and all( - com.is_null_slice(idx) or com.is_full_slice(idx, len(self.obj)) - for idx in pi - ): - s = v - else: - # set the item, possibly having a dtype change - s._consolidate_inplace() - s = s.copy() - s._data = s._data.setitem(indexer=pi, value=v) - s._maybe_update_cacher(clear=True) + def _has_valid_setitem_indexer(self, indexer) -> bool: + raise AbstractMethodError(self) - # reset the sliced object if unique - self.obj[item] = s + def _getbool_axis(self, key, axis: int): + # caller is responsible for ensuring non-None axis + labels = self.obj._get_axis(axis) + key = check_bool_indexer(labels, key) + inds = key.nonzero()[0] + return self.obj._take_with_is_copy(inds, axis=axis) - # we need an iterable, with a ndim of at least 1 - # eg. don't pass through np.array(0) - if is_list_like_indexer(value) and getattr(value, "ndim", 1) > 0: - # we have an equal len Frame - if isinstance(value, ABCDataFrame): - sub_indexer = list(indexer) - multiindex_indexer = isinstance(labels, ABCMultiIndex) +@Appender(IndexingMixin.loc.__doc__) +class _LocIndexer(_LocationIndexer): + _takeable: bool = False + _valid_types = ( + "labels (MUST BE IN THE INDEX), slices of labels (BOTH " + "endpoints included! Can be slices of integers if the " + "index is integers), listlike of labels, boolean" + ) - for item in labels: - if item in value: - sub_indexer[info_axis] = item - v = self._align_series( - tuple(sub_indexer), value[item], multiindex_indexer - ) - else: - v = np.nan + # ------------------------------------------------------------------- + # Key Checks - setter(item, v) + @Appender(_LocationIndexer._validate_key.__doc__) + def _validate_key(self, key, axis: int): - # we have an equal len ndarray/convertible to our labels - # hasattr first, to avoid coercing to ndarray without reason. - # But we may be relying on the ndarray coercion to check ndim. - # Why not just convert to an ndarray earlier on if needed? - elif np.ndim(value) == 2: - - # note that this coerces the dtype if we are mixed - # GH 7551 - value = np.array(value, dtype=object) - if len(labels) != value.shape[1]: - raise ValueError( - "Must have equal len keys and value " - "when setting with an ndarray" - ) - - for i, item in enumerate(labels): - - # setting with a list, recoerces - setter(item, value[:, i].tolist()) + # valid for a collection of labels (we check their presence later) + # slice of labels (where start-end in labels) + # slice of integers (only if in the labels) + # boolean - # we have an equal len list/ndarray - elif _can_do_equal_len( - labels, value, plane_indexer, lplane_indexer, self.obj - ): - setter(labels[0], value) + if isinstance(key, slice): + return - # per label values - else: + if com.is_bool_indexer(key): + return - if len(labels) != len(value): - raise ValueError( - "Must have equal len keys and value " - "when setting with an iterable" - ) + if not is_list_like_indexer(key): + labels = self.obj._get_axis(axis) + labels._convert_scalar_indexer(key, kind="loc") - for item, v in zip(labels, value): - setter(item, v) - else: + def _has_valid_setitem_indexer(self, indexer) -> bool: + return True - # scalar - for item in labels: - setter(item, value) + def _is_scalar_access(self, key: Tuple) -> bool: + """ + Returns + ------- + bool + """ + # this is a shortcut accessor to both .loc and .iloc + # that provide the equivalent access of .at and .iat + # a) avoid getting things via sections and (to minimize dtype changes) + # b) provide a performant path + if len(key) != self.ndim: + return False - else: - if isinstance(indexer, tuple): - indexer = maybe_convert_ix(*indexer) + for i, k in enumerate(key): + if not is_scalar(k): + return False - # if we are setting on the info axis ONLY - # set using those methods to avoid block-splitting - # logic here - if ( - len(indexer) > info_axis - and is_integer(indexer[info_axis]) - and all( - com.is_null_slice(idx) - for i, idx in enumerate(indexer) - if i != info_axis - ) - and item_labels.is_unique - ): - self.obj[item_labels[indexer[info_axis]]] = value - return + ax = self.obj.axes[i] + if isinstance(ax, ABCMultiIndex): + return False - if isinstance(value, (ABCSeries, dict)): - # TODO(EA): ExtensionBlock.setitem this causes issues with - # setting for extensionarrays that store dicts. Need to decide - # if it's worth supporting that. - value = self._align_series(indexer, Series(value)) + if isinstance(k, str) and ax._supports_partial_string_indexing: + # partial string indexing, df.loc['2000', 'A'] + # should not be considered scalar + return False - elif isinstance(value, ABCDataFrame): - value = self._align_frame(indexer, value) + if not ax.is_unique: + return False - # check for chained assignment - self.obj._check_is_chained_assignment_possible() + return True - # actually do the set - self.obj._consolidate_inplace() - self.obj._data = self.obj._data.setitem(indexer=indexer, value=value) - self.obj._maybe_update_cacher(clear=True) + # ------------------------------------------------------------------- + # MultiIndex Handling - def _setitem_with_indexer_missing(self, indexer, value): - """ - Insert new row(s) or column(s) into the Series or DataFrame. + def _multi_take_opportunity(self, tup: Tuple) -> bool: """ - from pandas import Series + Check whether there is the possibility to use ``_multi_take``. - # reindex the axis to the new value - # and set inplace - if self.ndim == 1: - index = self.obj.index - new_index = index.insert(len(index), indexer) + Currently the limit is that all axes being indexed, must be indexed with + list-likes. - # we have a coerced indexer, e.g. a float - # that matches in an Int64Index, so - # we will not create a duplicate index, rather - # index to that element - # e.g. 0.0 -> 0 - # GH#12246 - if index.is_unique: - new_indexer = index.get_indexer([new_index[-1]]) - if (new_indexer != -1).any(): - return self._setitem_with_indexer(new_indexer, value) + Parameters + ---------- + tup : tuple + Tuple of indexers, one per axis. - # this preserves dtype of the value - new_values = Series([value])._values - if len(self.obj._values): - # GH#22717 handle casting compatibility that np.concatenate - # does incorrectly - new_values = concat_compat([self.obj._values, new_values]) - self.obj._data = self.obj._constructor( - new_values, index=new_index, name=self.obj.name - )._data - self.obj._maybe_update_cacher(clear=True) - return self.obj + Returns + ------- + bool + Whether the current indexing, + can be passed through `_multi_take`. + """ + if not all(is_list_like_indexer(x) for x in tup): + return False - elif self.ndim == 2: + # just too complicated + if any(com.is_bool_indexer(x) for x in tup): + return False - if not len(self.obj.columns): - # no columns and scalar - raise ValueError("cannot set a frame with no defined columns") + return True - if isinstance(value, ABCSeries): - # append a Series - value = value.reindex(index=self.obj.columns, copy=True) - value.name = indexer + def _multi_take(self, tup: Tuple): + """ + Create the indexers for the passed tuple of keys, and + executes the take operation. This allows the take operation to be + executed all at once, rather than once for each dimension. + Improving efficiency. - else: - # a list-list - if is_list_like_indexer(value): - # must have conforming columns - if len(value) != len(self.obj.columns): - raise ValueError("cannot set a row with mismatched columns") + Parameters + ---------- + tup : tuple + Tuple of indexers, one per axis. - value = Series(value, index=self.obj.columns, name=indexer) + Returns + ------- + values: same type as the object being indexed + """ + # GH 836 + d = { + axis: self._get_listlike_indexer(key, axis) + for (key, axis) in zip(tup, self.obj._AXIS_ORDERS) + } + return self.obj._reindex_with_indexers(d, copy=True, allow_dups=True) - self.obj._data = self.obj.append(value)._data - self.obj._maybe_update_cacher(clear=True) - return self.obj + # ------------------------------------------------------------------- - def _align_series(self, indexer, ser: ABCSeries, multiindex_indexer: bool = False): + def _getitem_iterable(self, key, axis: int): """ + Index current object with an an iterable collection of keys. + Parameters ---------- - indexer : tuple, slice, scalar - Indexer used to get the locations that will be set to `ser`. - ser : pd.Series - Values to assign to the locations specified by `indexer`. - multiindex_indexer : boolean, optional - Defaults to False. Should be set to True if `indexer` was from - a `pd.MultiIndex`, to avoid unnecessary broadcasting. + key : iterable + Targeted labels. + axis: int + Dimension on which the indexing is being made. + + Raises + ------ + KeyError + If no key was found. Will change in the future to raise if not all + keys were found. Returns ------- - `np.array` of `ser` broadcast to the appropriate shape for assignment - to the locations selected by `indexer` + scalar, DataFrame, or Series: indexed value(s). """ - if isinstance(indexer, (slice, np.ndarray, list, Index)): - indexer = tuple([indexer]) - - if isinstance(indexer, tuple): + # we assume that not com.is_bool_indexer(key), as that is + # handled before we get here. + self._validate_key(key, axis) - # flatten np.ndarray indexers - def ravel(i): - return i.ravel() if isinstance(i, np.ndarray) else i + # A collection of keys + keyarr, indexer = self._get_listlike_indexer(key, axis, raise_missing=False) + return self.obj._reindex_with_indexers( + {axis: [keyarr, indexer]}, copy=True, allow_dups=True + ) - indexer = tuple(map(ravel, indexer)) + def _getitem_tuple(self, tup: Tuple): + try: + return self._getitem_lowerdim(tup) + except IndexingError: + pass - aligners = [not com.is_null_slice(idx) for idx in indexer] - sum_aligners = sum(aligners) - single_aligner = sum_aligners == 1 - is_frame = self.ndim == 2 - obj = self.obj + # no multi-index, so validate all of the indexers + self._has_valid_tuple(tup) - # are we a single alignable value on a non-primary - # dim (e.g. panel: 1,2, or frame: 0) ? - # hence need to align to a single axis dimension - # rather that find all valid dims + # ugly hack for GH #836 + if self._multi_take_opportunity(tup): + return self._multi_take(tup) - # frame - if is_frame: - single_aligner = single_aligner and aligners[0] + return self._getitem_tuple_same_dim(tup) - # we have a frame, with multiple indexers on both axes; and a - # series, so need to broadcast (see GH5206) - if sum_aligners == self.ndim and all(is_sequence(_) for _ in indexer): - ser = ser.reindex(obj.axes[0][indexer[0]], copy=True)._values - - # single indexer - if len(indexer) > 1 and not multiindex_indexer: - len_indexer = len(indexer[1]) - ser = np.tile(ser, len_indexer).reshape(len_indexer, -1).T - - return ser - - for i, idx in enumerate(indexer): - ax = obj.axes[i] - - # multiple aligners (or null slices) - if is_sequence(idx) or isinstance(idx, slice): - if single_aligner and com.is_null_slice(idx): - continue - new_ix = ax[idx] - if not is_list_like_indexer(new_ix): - new_ix = Index([new_ix]) - else: - new_ix = Index(new_ix) - if ser.index.equals(new_ix) or not len(new_ix): - return ser._values.copy() - - return ser.reindex(new_ix)._values - - # 2 dims - elif single_aligner: - - # reindex along index - ax = self.obj.axes[1] - if ser.index.equals(ax) or not len(ax): - return ser._values.copy() - return ser.reindex(ax)._values - - elif is_scalar(indexer): - ax = self.obj._get_axis(1) - - if ser.index.equals(ax): - return ser._values.copy() - - return ser.reindex(ax)._values - - raise ValueError("Incompatible indexer with Series") - - def _align_frame(self, indexer, df: ABCDataFrame): - is_frame = self.ndim == 2 - - if isinstance(indexer, tuple): - - idx, cols = None, None - sindexers = [] - for i, ix in enumerate(indexer): - ax = self.obj.axes[i] - if is_sequence(ix) or isinstance(ix, slice): - if isinstance(ix, np.ndarray): - ix = ix.ravel() - if idx is None: - idx = ax[ix] - elif cols is None: - cols = ax[ix] - else: - break - else: - sindexers.append(i) - - if idx is not None and cols is not None: - - if df.index.equals(idx) and df.columns.equals(cols): - val = df.copy()._values - else: - val = df.reindex(idx, columns=cols)._values - return val - - elif (isinstance(indexer, slice) or is_list_like_indexer(indexer)) and is_frame: - ax = self.obj.index[indexer] - if df.index.equals(ax): - val = df.copy()._values - else: - - # we have a multi-index and are trying to align - # with a particular, level GH3738 - if ( - isinstance(ax, ABCMultiIndex) - and isinstance(df.index, ABCMultiIndex) - and ax.nlevels != df.index.nlevels - ): - raise TypeError( - "cannot align on a multi-index with out " - "specifying the join levels" - ) - - val = df.reindex(index=ax)._values - return val - - raise ValueError("Incompatible indexer with DataFrame") + def _get_label(self, label, axis: int): + # GH#5667 this will fail if the label is not present in the axis. + return self.obj._xs(label, axis=axis) def _handle_lowerdim_multi_index_axis0(self, tup: Tuple): # we have an axis0 multi-index, handle or raise @@ -1201,108 +1033,218 @@ def _handle_lowerdim_multi_index_axis0(self, tup: Tuple): return None - def _getitem_lowerdim(self, tup: Tuple): - - # we can directly get the axis result since the axis is specified - if self.axis is not None: - axis = self.obj._get_axis_number(self.axis) - return self._getitem_axis(tup, axis=axis) + def _getitem_axis(self, key, axis: int): + key = item_from_zerodim(key) + if is_iterator(key): + key = list(key) - # we may have a nested tuples indexer here - if self._is_nested_tuple_indexer(tup): - return self._getitem_nested_tuple(tup) + labels = self.obj._get_axis(axis) + key = labels._get_partial_string_timestamp_match_key(key) - # we maybe be using a tuple to represent multiple dimensions here - ax0 = self.obj._get_axis(0) - # ...but iloc should handle the tuple as simple integer-location - # instead of checking it as multiindex representation (GH 13797) - if isinstance(ax0, ABCMultiIndex) and self.name != "iloc": - result = self._handle_lowerdim_multi_index_axis0(tup) - if result is not None: - return result + if isinstance(key, slice): + self._validate_key(key, axis) + return self._get_slice_axis(key, axis=axis) + elif com.is_bool_indexer(key): + return self._getbool_axis(key, axis=axis) + elif is_list_like_indexer(key): - if len(tup) > self.ndim: - raise IndexingError("Too many indexers. handle elsewhere") + # convert various list-like indexers + # to a list of keys + # we will use the *values* of the object + # and NOT the index if its a PandasObject + if isinstance(labels, ABCMultiIndex): - for i, key in enumerate(tup): - if is_label_like(key) or isinstance(key, tuple): - section = self._getitem_axis(key, axis=i) + if isinstance(key, (ABCSeries, np.ndarray)) and key.ndim <= 1: + # Series, or 0,1 ndim ndarray + # GH 14730 + key = list(key) + elif isinstance(key, ABCDataFrame): + # GH 15438 + raise NotImplementedError( + "Indexing a MultiIndex with a " + "DataFrame key is not " + "implemented" + ) + elif hasattr(key, "ndim") and key.ndim > 1: + raise NotImplementedError( + "Indexing a MultiIndex with a " + "multidimensional key is not " + "implemented" + ) - # we have yielded a scalar ? - if not is_list_like_indexer(section): - return section + if ( + not isinstance(key, tuple) + and len(key) + and not isinstance(key[0], tuple) + ): + key = tuple([key]) - elif section.ndim == self.ndim: - # we're in the middle of slicing through a MultiIndex - # revise the key wrt to `section` by inserting an _NS - new_key = tup[:i] + (_NS,) + tup[i + 1 :] + # an iterable multi-selection + if not (isinstance(key, tuple) and isinstance(labels, ABCMultiIndex)): - else: - new_key = tup[:i] + tup[i + 1 :] + if hasattr(key, "ndim") and key.ndim > 1: + raise ValueError("Cannot index with multidimensional key") - # unfortunately need an odious kludge here because of - # DataFrame transposing convention - if ( - isinstance(section, ABCDataFrame) - and i > 0 - and len(new_key) == 2 - ): - a, b = new_key - new_key = b, a + return self._getitem_iterable(key, axis=axis) - if len(new_key) == 1: - new_key = new_key[0] + # nested tuple slicing + if is_nested_tuple(key, labels): + locs = labels.get_locs(key) + indexer = [slice(None)] * self.ndim + indexer[axis] = locs + return self.obj.iloc[tuple(indexer)] - # Slices should return views, but calling iloc/loc with a null - # slice returns a new object. - if com.is_null_slice(new_key): - return section - # This is an elided recursive call to iloc/loc/etc' - return getattr(section, self.name)[new_key] + # fall thru to straight lookup + self._validate_key(key, axis) + return self._get_label(key, axis=axis) - raise IndexingError("not applicable") + def _get_slice_axis(self, slice_obj: slice, axis: int): + """ + This is pretty simple as we just have to deal with labels. + """ + # caller is responsible for ensuring non-None axis + obj = self.obj + if not need_slice(slice_obj): + return obj.copy(deep=False) - def _getitem_nested_tuple(self, tup: Tuple): - # we have a nested tuple so have at least 1 multi-index level - # we should be able to match up the dimensionality here + labels = obj._get_axis(axis) + indexer = labels.slice_indexer( + slice_obj.start, slice_obj.stop, slice_obj.step, kind="loc" + ) - # we have too many indexers for our dim, but have at least 1 - # multi-index dimension, try to see if we have something like - # a tuple passed to a series with a multi-index - if len(tup) > self.ndim: - result = self._handle_lowerdim_multi_index_axis0(tup) - if result is not None: - return result + if isinstance(indexer, slice): + return self.obj._slice(indexer, axis=axis) + else: + # DatetimeIndex overrides Index.slice_indexer and may + # return a DatetimeIndex instead of a slice object. + return self.obj.take(indexer, axis=axis) - # this is a series with a multi-index specified a tuple of - # selectors - axis = self.axis or 0 - return self._getitem_axis(tup, axis=axis) + def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): + """ + Convert indexing key into something we can use to do actual fancy + indexing on a ndarray. - # handle the multi-axis by taking sections and reducing - # this is iterative - obj = self.obj - axis = 0 - for i, key in enumerate(tup): + Examples + ix[:5] -> slice(0, 5) + ix[[1,2,3]] -> [1,2,3] + ix[['foo', 'bar', 'baz']] -> [i, j, k] (indices of foo, bar, baz) - if com.is_null_slice(key): - axis += 1 - continue + Going by Zen of Python? + 'In the face of ambiguity, refuse the temptation to guess.' + raise AmbiguousIndexError with integer labels? + - No, prefer label-based indexing + """ + labels = self.obj._get_axis(axis) - current_ndim = obj.ndim - obj = getattr(obj, self.name)._getitem_axis(key, axis=axis) - axis += 1 + if isinstance(key, slice): + return labels._convert_slice_indexer(key, kind="loc") - # if we have a scalar, we are done - if is_scalar(obj) or not hasattr(obj, "ndim"): - break + if is_scalar(key): + # try to find out correct indexer, if not type correct raise + try: + key = labels._convert_scalar_indexer(key, kind="loc") + except KeyError: + # but we will allow setting + if not is_setter: + raise - # has the dim of the obj changed? - # GH 7199 - if obj.ndim < current_ndim: - axis -= 1 + # see if we are positional in nature + is_int_index = labels.is_integer() + is_int_positional = is_integer(key) and not is_int_index - return obj + if is_scalar(key) or isinstance(labels, ABCMultiIndex): + # Otherwise get_loc will raise InvalidIndexError + + # if we are a label return me + try: + return labels.get_loc(key) + except LookupError: + if isinstance(key, tuple) and isinstance(labels, ABCMultiIndex): + if len(key) == labels.nlevels: + return {"key": key} + raise + except TypeError: + pass + except ValueError: + if not is_int_positional: + raise + + # a positional + if is_int_positional: + + # if we are setting and its not a valid location + # its an insert which fails by definition + + # always valid + return {"key": key} + + if is_nested_tuple(key, labels): + return labels.get_locs(key) + + elif is_list_like_indexer(key): + + if com.is_bool_indexer(key): + key = check_bool_indexer(labels, key) + (inds,) = key.nonzero() + return inds + else: + # When setting, missing keys are not allowed, even with .loc: + return self._get_listlike_indexer(key, axis, raise_missing=True)[1] + else: + try: + return labels.get_loc(key) + except LookupError: + # allow a not found key only if we are a setter + if not is_list_like_indexer(key): + return {"key": key} + raise + + def _get_listlike_indexer(self, key, axis: int, raise_missing: bool = False): + """ + Transform a list-like of keys into a new index and an indexer. + + Parameters + ---------- + key : list-like + Targeted labels. + axis: int + Dimension on which the indexing is being made. + raise_missing: bool, default False + Whether to raise a KeyError if some labels were not found. + Will be removed in the future, and then this method will always behave as + if ``raise_missing=True``. + + Raises + ------ + KeyError + If at least one key was requested but none was found, and + raise_missing=True. + + Returns + ------- + keyarr: Index + New index (coinciding with 'key' if the axis is unique). + values : array-like + Indexer for the return object, -1 denotes keys not found. + """ + ax = self.obj._get_axis(axis) + + # Have the index compute an indexer or return None + # if it cannot handle: + indexer, keyarr = ax._convert_listlike_indexer(key) + # We only act on all found values: + if indexer is not None and (indexer != -1).all(): + self._validate_read_indexer(key, indexer, axis, raise_missing=raise_missing) + return ax[indexer], indexer + + if ax.is_unique and not getattr(ax, "is_overlapping", False): + indexer = ax.get_indexer_for(key) + keyarr = ax.reindex(keyarr)[0] + else: + keyarr, indexer, new_indexer = ax._reindex_non_unique(keyarr) + + self._validate_read_indexer(keyarr, indexer, axis, raise_missing=raise_missing) + return keyarr, indexer def _validate_read_indexer( self, key, indexer, axis: int, raise_missing: bool = False @@ -1348,93 +1290,98 @@ def _validate_read_indexer( # We (temporarily) allow for some missing keys with .loc, except in # some cases (e.g. setting) in which "raise_missing" will be False - if not (self.name == "loc" and not raise_missing): + if raise_missing: not_found = list(set(key) - set(ax)) raise KeyError(f"{not_found} not in index") - # we skip the warning on Categorical/Interval + # we skip the warning on Categorical # as this check is actually done (check for # non-missing values), but a bit later in the # code, so we want to avoid warning & then # just raising - if not (ax.is_categorical() or ax.is_interval()): + if not ax.is_categorical(): raise KeyError( "Passing list-likes to .loc or [] with any missing labels " "is no longer supported, see " "https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#deprecate-loc-reindex-listlike" # noqa:E501 ) - def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): - raise AbstractMethodError(self) - - def __getitem__(self, key): - if type(key) is tuple: - key = tuple(com.apply_if_callable(x, self.obj) for x in key) - if self._is_scalar_access(key): - try: - return self.obj._get_value(*key, takeable=self._takeable) - except (KeyError, IndexError, AttributeError): - # AttributeError for IntervalTree get_value - pass - return self._getitem_tuple(key) - else: - # we by definition only have the 0th axis - axis = self.axis or 0 - - maybe_callable = com.apply_if_callable(key, self.obj) - return self._getitem_axis(maybe_callable, axis=axis) - - def _is_scalar_access(self, key: Tuple): - raise NotImplementedError() - - def _getitem_tuple(self, tup: Tuple): - raise AbstractMethodError(self) - - def _getitem_axis(self, key, axis: int): - raise NotImplementedError() - - def _has_valid_setitem_indexer(self, indexer) -> bool: - raise AbstractMethodError(self) - - def _getbool_axis(self, key, axis: int): - # caller is responsible for ensuring non-None axis - labels = self.obj._get_axis(axis) - key = check_bool_indexer(labels, key) - inds = key.nonzero()[0] - return self.obj._take_with_is_copy(inds, axis=axis) - -@Appender(IndexingMixin.loc.__doc__) -class _LocIndexer(_LocationIndexer): - _takeable: bool = False +@Appender(IndexingMixin.iloc.__doc__) +class _iLocIndexer(_LocationIndexer): _valid_types = ( - "labels (MUST BE IN THE INDEX), slices of labels (BOTH " - "endpoints included! Can be slices of integers if the " - "index is integers), listlike of labels, boolean" + "integer, integer slice (START point is INCLUDED, END " + "point is EXCLUDED), listlike of integers, boolean array" ) + _takeable = True # ------------------------------------------------------------------- # Key Checks - @Appender(_LocationIndexer._validate_key.__doc__) def _validate_key(self, key, axis: int): - - # valid for a collection of labels (we check their presence later) - # slice of labels (where start-end in labels) - # slice of integers (only if in the labels) - # boolean + if com.is_bool_indexer(key): + if hasattr(key, "index") and isinstance(key.index, Index): + if key.index.inferred_type == "integer": + raise NotImplementedError( + "iLocation based boolean " + "indexing on an integer type " + "is not available" + ) + raise ValueError( + "iLocation based boolean indexing cannot use " + "an indexable as a mask" + ) + return if isinstance(key, slice): return + elif is_integer(key): + self._validate_integer(key, axis) + elif isinstance(key, tuple): + # a tuple should already have been caught by this point + # so don't treat a tuple as a valid indexer + raise IndexingError("Too many indexers") + elif is_list_like_indexer(key): + arr = np.array(key) + len_axis = len(self.obj._get_axis(axis)) - if com.is_bool_indexer(key): - return + # check that the key has a numeric dtype + if not is_numeric_dtype(arr.dtype): + raise IndexError(f".iloc requires numeric indexers, got {arr}") - if not is_list_like_indexer(key): - labels = self.obj._get_axis(axis) - labels._convert_scalar_indexer(key, kind="loc") + # check that the key does not exceed the maximum size of the index + if len(arr) and (arr.max() >= len_axis or arr.min() < -len_axis): + raise IndexError("positional indexers are out-of-bounds") + else: + raise ValueError(f"Can only index by location with a [{self._valid_types}]") def _has_valid_setitem_indexer(self, indexer) -> bool: + """ + Validate that a positional indexer cannot enlarge its target + will raise if needed, does not modify the indexer externally. + + Returns + ------- + bool + """ + if isinstance(indexer, dict): + raise IndexError("iloc cannot enlarge its target object") + else: + if not isinstance(indexer, tuple): + indexer = _tuplify(self.ndim, indexer) + for ax, i in zip(self.obj.axes, indexer): + if isinstance(i, slice): + # should check the stop slice? + pass + elif is_list_like_indexer(i): + # should check the elements? + pass + elif is_integer(i): + if i >= len(ax): + raise IndexError("iloc cannot enlarge its target object") + elif isinstance(i, dict): + raise IndexError("iloc cannot enlarge its target object") + return True def _is_scalar_access(self, key: Tuple) -> bool: @@ -1451,609 +1398,588 @@ def _is_scalar_access(self, key: Tuple) -> bool: return False for i, k in enumerate(key): - if not is_scalar(k): - return False - - ax = self.obj.axes[i] - if isinstance(ax, ABCMultiIndex): - return False - - if isinstance(k, str) and ax._supports_partial_string_indexing: - # partial string indexing, df.loc['2000', 'A'] - # should not be considered scalar - return False - - if not ax.is_unique: + if not is_integer(k): return False return True - # ------------------------------------------------------------------- - # MultiIndex Handling - - def _multi_take_opportunity(self, tup: Tuple) -> bool: + def _validate_integer(self, key: int, axis: int) -> None: """ - Check whether there is the possibility to use ``_multi_take``. - - Currently the limit is that all axes being indexed, must be indexed with - list-likes. + Check that 'key' is a valid position in the desired axis. Parameters ---------- - tup : tuple - Tuple of indexers, one per axis. + key : int + Requested position. + axis : int + Desired axis. - Returns - ------- - bool - Whether the current indexing, - can be passed through `_multi_take`. + Raises + ------ + IndexError + If 'key' is not a valid position in axis 'axis'. """ - if not all(is_list_like_indexer(x) for x in tup): - return False - - # just too complicated - if any(com.is_bool_indexer(x) for x in tup): - return False - - return True - - def _multi_take(self, tup: Tuple): - """ - Create the indexers for the passed tuple of keys, and - executes the take operation. This allows the take operation to be - executed all at once, rather than once for each dimension. - Improving efficiency. - - Parameters - ---------- - tup : tuple - Tuple of indexers, one per axis. - - Returns - ------- - values: same type as the object being indexed - """ - # GH 836 - d = { - axis: self._get_listlike_indexer(key, axis) - for (key, axis) in zip(tup, self.obj._AXIS_ORDERS) - } - return self.obj._reindex_with_indexers(d, copy=True, allow_dups=True) + len_axis = len(self.obj._get_axis(axis)) + if key >= len_axis or key < -len_axis: + raise IndexError("single positional indexer is out-of-bounds") # ------------------------------------------------------------------- - def _get_partial_string_timestamp_match_key(self, key, labels): - """ - Translate any partial string timestamp matches in key, returning the - new key. + def _getitem_tuple(self, tup: Tuple): - (GH 10331) - """ - if isinstance(labels, ABCMultiIndex): - if ( - isinstance(key, str) - and labels.levels[0]._supports_partial_string_indexing - ): - # Convert key '2016-01-01' to - # ('2016-01-01'[, slice(None, None, None)]+) - key = tuple([key] + [slice(None)] * (len(labels.levels) - 1)) - - if isinstance(key, tuple): - # Convert (..., '2016-01-01', ...) in tuple to - # (..., slice('2016-01-01', '2016-01-01', None), ...) - new_key = [] - for i, component in enumerate(key): - if ( - isinstance(component, str) - and labels.levels[i]._supports_partial_string_indexing - ): - new_key.append(slice(component, component, None)) - else: - new_key.append(component) - key = tuple(new_key) + self._has_valid_tuple(tup) + try: + return self._getitem_lowerdim(tup) + except IndexingError: + pass - return key + return self._getitem_tuple_same_dim(tup) - def _getitem_iterable(self, key, axis: int): + def _get_list_axis(self, key, axis: int): """ - Index current object with an an iterable collection of keys. + Return Series values by list or array of integers. Parameters ---------- - key : iterable - Targeted labels. - axis: int - Dimension on which the indexing is being made. - - Raises - ------ - KeyError - If no key was found. Will change in the future to raise if not all - keys were found. + key : list-like positional indexer + axis : int Returns ------- - scalar, DataFrame, or Series: indexed value(s). - """ - # we assume that not com.is_bool_indexer(key), as that is - # handled before we get here. - self._validate_key(key, axis) - - # A collection of keys - keyarr, indexer = self._get_listlike_indexer(key, axis, raise_missing=False) - return self.obj._reindex_with_indexers( - {axis: [keyarr, indexer]}, copy=True, allow_dups=True - ) + Series object - def _getitem_tuple(self, tup: Tuple): + Notes + ----- + `axis` can only be zero. + """ try: - return self._getitem_lowerdim(tup) - except IndexingError: - pass - - # no multi-index, so validate all of the indexers - self._has_valid_tuple(tup) - - # ugly hack for GH #836 - if self._multi_take_opportunity(tup): - return self._multi_take(tup) - - # no shortcut needed - retval = self.obj - for i, key in enumerate(tup): - if com.is_null_slice(key): - continue - - retval = getattr(retval, self.name)._getitem_axis(key, axis=i) - - return retval + return self.obj._take_with_is_copy(key, axis=axis) + except IndexError: + # re-raise with different error message + raise IndexError("positional indexers are out-of-bounds") def _getitem_axis(self, key, axis: int): - key = item_from_zerodim(key) - if is_iterator(key): - key = list(key) - - labels = self.obj._get_axis(axis) - key = self._get_partial_string_timestamp_match_key(key, labels) - if isinstance(key, slice): - self._validate_key(key, axis) return self._get_slice_axis(key, axis=axis) - elif com.is_bool_indexer(key): - return self._getbool_axis(key, axis=axis) - elif is_list_like_indexer(key): - - # convert various list-like indexers - # to a list of keys - # we will use the *values* of the object - # and NOT the index if its a PandasObject - if isinstance(labels, ABCMultiIndex): - - if isinstance(key, (ABCSeries, np.ndarray)) and key.ndim <= 1: - # Series, or 0,1 ndim ndarray - # GH 14730 - key = list(key) - elif isinstance(key, ABCDataFrame): - # GH 15438 - raise NotImplementedError( - "Indexing a MultiIndex with a " - "DataFrame key is not " - "implemented" - ) - elif hasattr(key, "ndim") and key.ndim > 1: - raise NotImplementedError( - "Indexing a MultiIndex with a " - "multidimensional key is not " - "implemented" - ) - if ( - not isinstance(key, tuple) - and len(key) - and not isinstance(key[0], tuple) - ): - key = tuple([key]) + if isinstance(key, list): + key = np.asarray(key) - # an iterable multi-selection - if not (isinstance(key, tuple) and isinstance(labels, ABCMultiIndex)): + if com.is_bool_indexer(key): + self._validate_key(key, axis) + return self._getbool_axis(key, axis=axis) - if hasattr(key, "ndim") and key.ndim > 1: - raise ValueError("Cannot index with multidimensional key") + # a list of integers + elif is_list_like_indexer(key): + return self._get_list_axis(key, axis=axis) - return self._getitem_iterable(key, axis=axis) + # a single integer + else: + key = item_from_zerodim(key) + if not is_integer(key): + raise TypeError("Cannot index by location index with a non-integer key") - # nested tuple slicing - if is_nested_tuple(key, labels): - locs = labels.get_locs(key) - indexer = [slice(None)] * self.ndim - indexer[axis] = locs - return self.obj.iloc[tuple(indexer)] + # validate the location + self._validate_integer(key, axis) - # fall thru to straight lookup - self._validate_key(key, axis) - return self._get_label(key, axis=axis) + return self.obj._ixs(key, axis=axis) def _get_slice_axis(self, slice_obj: slice, axis: int): - """ - This is pretty simple as we just have to deal with labels. - """ # caller is responsible for ensuring non-None axis obj = self.obj + if not need_slice(slice_obj): return obj.copy(deep=False) labels = obj._get_axis(axis) - indexer = labels.slice_indexer( - slice_obj.start, slice_obj.stop, slice_obj.step, kind=self.name - ) - - if isinstance(indexer, slice): - return self.obj._slice(indexer, axis=axis, kind="iloc") - else: - # DatetimeIndex overrides Index.slice_indexer and may - # return a DatetimeIndex instead of a slice object. - return self.obj.take(indexer, axis=axis) + labels._validate_positional_slice(slice_obj) + return self.obj._slice(slice_obj, axis=axis) def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): """ - Convert indexing key into something we can use to do actual fancy - indexing on a ndarray. - - Examples - ix[:5] -> slice(0, 5) - ix[[1,2,3]] -> [1,2,3] - ix[['foo', 'bar', 'baz']] -> [i, j, k] (indices of foo, bar, baz) - - Going by Zen of Python? - 'In the face of ambiguity, refuse the temptation to guess.' - raise AmbiguousIndexError with integer labels? - - No, prefer label-based indexing + Much simpler as we only have to deal with our valid types. """ - labels = self.obj._get_axis(axis) - - if isinstance(key, slice): - return labels._convert_slice_indexer(key, kind="loc") + return key - if is_scalar(key): - # try to find out correct indexer, if not type correct raise - try: - key = labels._convert_scalar_indexer(key, kind="loc") - except TypeError: - # but we will allow setting - if not is_setter: - raise + def _get_setitem_indexer(self, key): + # GH#32257 Fall through to let numnpy do validation + return key - # see if we are positional in nature - is_int_index = labels.is_integer() - is_int_positional = is_integer(key) and not is_int_index + # ------------------------------------------------------------------- - if is_scalar(key) or isinstance(labels, ABCMultiIndex): - # Otherwise get_loc will raise InvalidIndexError + def _setitem_with_indexer(self, indexer, value): + """ + _setitem_with_indexer is for setting values on a Series/DataFrame + using positional indexers. - # if we are a label return me - try: - return labels.get_loc(key) - except LookupError: - if isinstance(key, tuple) and isinstance(labels, ABCMultiIndex): - if len(key) == labels.nlevels: - return {"key": key} - raise - except TypeError: - pass - except ValueError: - if not is_int_positional: - raise + If the relevant keys are not present, the Series/DataFrame may be + expanded. - # a positional - if is_int_positional: + This method is currently broken when dealing with non-unique Indexes, + since it goes from positional indexers back to labels when calling + BlockManager methods, see GH#12991, GH#22046, GH#15686. + """ - # if we are setting and its not a valid location - # its an insert which fails by definition + # also has the side effect of consolidating in-place + from pandas import Series - # always valid - return {"key": key} + info_axis = self.obj._info_axis_number - if is_nested_tuple(key, labels): - return labels.get_locs(key) + # maybe partial set + take_split_path = self.obj._is_mixed_type - elif is_list_like_indexer(key): + # if there is only one block/type, still have to take split path + # unless the block is one-dimensional or it can hold the value + if not take_split_path and self.obj._data.blocks: + (blk,) = self.obj._data.blocks + if 1 < blk.ndim: # in case of dict, keys are indices + val = list(value.values()) if isinstance(value, dict) else value + take_split_path = not blk._can_hold_element(val) - if com.is_bool_indexer(key): - key = check_bool_indexer(labels, key) - (inds,) = key.nonzero() - return inds - else: - # When setting, missing keys are not allowed, even with .loc: - return self._get_listlike_indexer(key, axis, raise_missing=True)[1] - else: - try: - return labels.get_loc(key) - except LookupError: - # allow a not found key only if we are a setter - if not is_list_like_indexer(key): - return {"key": key} - raise + # if we have any multi-indexes that have non-trivial slices + # (not null slices) then we must take the split path, xref + # GH 10360, GH 27841 + if isinstance(indexer, tuple) and len(indexer) == len(self.obj.axes): + for i, ax in zip(indexer, self.obj.axes): + if isinstance(ax, ABCMultiIndex) and not ( + is_integer(i) or com.is_null_slice(i) + ): + take_split_path = True + break - def _get_listlike_indexer(self, key, axis: int, raise_missing: bool = False): - """ - Transform a list-like of keys into a new index and an indexer. + if isinstance(indexer, tuple): + nindexer = [] + for i, idx in enumerate(indexer): + if isinstance(idx, dict): - Parameters - ---------- - key : list-like - Targeted labels. - axis: int - Dimension on which the indexing is being made. - raise_missing: bool, default False - Whether to raise a KeyError if some labels were not found. - Will be removed in the future, and then this method will always behave as - if ``raise_missing=True``. + # reindex the axis to the new value + # and set inplace + key, _ = convert_missing_indexer(idx) - Raises - ------ - KeyError - If at least one key was requested but none was found, and - raise_missing=True. + # if this is the items axes, then take the main missing + # path first + # this correctly sets the dtype and avoids cache issues + # essentially this separates out the block that is needed + # to possibly be modified + if self.ndim > 1 and i == info_axis: - Returns - ------- - keyarr: Index - New index (coinciding with 'key' if the axis is unique). - values : array-like - Indexer for the return object, -1 denotes keys not found. - """ - ax = self.obj._get_axis(axis) + # add the new item, and set the value + # must have all defined axes if we have a scalar + # or a list-like on the non-info axes if we have a + # list-like + len_non_info_axes = ( + len(_ax) for _i, _ax in enumerate(self.obj.axes) if _i != i + ) + if any(not l for l in len_non_info_axes): + if not is_list_like_indexer(value): + raise ValueError( + "cannot set a frame with no " + "defined index and a scalar" + ) + self.obj[key] = value + return - # Have the index compute an indexer or return None - # if it cannot handle: - indexer, keyarr = ax._convert_listlike_indexer(key) - # We only act on all found values: - if indexer is not None and (indexer != -1).all(): - self._validate_read_indexer(key, indexer, axis, raise_missing=raise_missing) - return ax[indexer], indexer + # add a new item with the dtype setup + self.obj[key] = _infer_fill_value(value) - if ax.is_unique and not getattr(ax, "is_overlapping", False): - indexer = ax.get_indexer_for(key) - keyarr = ax.reindex(keyarr)[0] + new_indexer = convert_from_missing_indexer_tuple( + indexer, self.obj.axes + ) + self._setitem_with_indexer(new_indexer, value) + + return + + # reindex the axis + # make sure to clear the cache because we are + # just replacing the block manager here + # so the object is the same + index = self.obj._get_axis(i) + labels = index.insert(len(index), key) + self.obj._data = self.obj.reindex(labels, axis=i)._data + self.obj._maybe_update_cacher(clear=True) + self.obj._is_copy = None + + nindexer.append(labels.get_loc(key)) + + else: + nindexer.append(idx) + + indexer = tuple(nindexer) else: - keyarr, indexer, new_indexer = ax._reindex_non_unique(keyarr) - self._validate_read_indexer(keyarr, indexer, axis, raise_missing=raise_missing) - return keyarr, indexer + indexer, missing = convert_missing_indexer(indexer) + if missing: + self._setitem_with_indexer_missing(indexer, value) + return -@Appender(IndexingMixin.iloc.__doc__) -class _iLocIndexer(_LocationIndexer): - _valid_types = ( - "integer, integer slice (START point is INCLUDED, END " - "point is EXCLUDED), listlike of integers, boolean array" - ) - _takeable = True + # set + item_labels = self.obj._get_axis(info_axis) - # ------------------------------------------------------------------- - # Key Checks + # align and set the values + if take_split_path: + # Above we only set take_split_path to True for 2D cases + assert self.ndim == 2 + assert info_axis == 1 - def _validate_key(self, key, axis: int): - if com.is_bool_indexer(key): - if hasattr(key, "index") and isinstance(key.index, Index): - if key.index.inferred_type == "integer": - raise NotImplementedError( - "iLocation based boolean " - "indexing on an integer type " - "is not available" + if not isinstance(indexer, tuple): + indexer = _tuplify(self.ndim, indexer) + + if isinstance(value, ABCSeries): + value = self._align_series(indexer, value) + + info_idx = indexer[info_axis] + if is_integer(info_idx): + info_idx = [info_idx] + labels = item_labels[info_idx] + + if len(labels) == 1: + # We can operate on a single column + item = labels[0] + idx = indexer[0] + + plane_indexer = tuple([idx]) + lplane_indexer = length_of_indexer(plane_indexer[0], self.obj.index) + # lplane_indexer gives the expected length of obj[idx] + + # require that we are setting the right number of values that + # we are indexing + if is_list_like_indexer(value) and 0 != lplane_indexer != len(value): + # Exclude zero-len for e.g. boolean masking that is all-false + raise ValueError( + "cannot set using a multi-index " + "selection indexer with a different " + "length than the value" ) - raise ValueError( - "iLocation based boolean indexing cannot use " - "an indexable as a mask" - ) - return - if isinstance(key, slice): - return - elif is_integer(key): - self._validate_integer(key, axis) - elif isinstance(key, tuple): - # a tuple should already have been caught by this point - # so don't treat a tuple as a valid indexer - raise IndexingError("Too many indexers") - elif is_list_like_indexer(key): - arr = np.array(key) - len_axis = len(self.obj._get_axis(axis)) + # non-mi + else: + plane_indexer = indexer[:1] + lplane_indexer = length_of_indexer(plane_indexer[0], self.obj.index) - # check that the key has a numeric dtype - if not is_numeric_dtype(arr.dtype): - raise IndexError(f".iloc requires numeric indexers, got {arr}") + def setter(item, v): + ser = self.obj[item] + pi = plane_indexer[0] if lplane_indexer == 1 else plane_indexer + + # perform the equivalent of a setitem on the info axis + # as we have a null slice or a slice with full bounds + # which means essentially reassign to the columns of a + # multi-dim object + # GH6149 (null slice), GH10408 (full bounds) + if isinstance(pi, tuple) and all( + com.is_null_slice(idx) or com.is_full_slice(idx, len(self.obj)) + for idx in pi + ): + ser = v + else: + # set the item, possibly having a dtype change + ser._consolidate_inplace() + ser = ser.copy() + ser._data = ser._data.setitem(indexer=pi, value=v) + ser._maybe_update_cacher(clear=True) + + # reset the sliced object if unique + self.obj[item] = ser + + # we need an iterable, with a ndim of at least 1 + # eg. don't pass through np.array(0) + if is_list_like_indexer(value) and getattr(value, "ndim", 1) > 0: + + # we have an equal len Frame + if isinstance(value, ABCDataFrame): + sub_indexer = list(indexer) + multiindex_indexer = isinstance(labels, ABCMultiIndex) + + for item in labels: + if item in value: + sub_indexer[info_axis] = item + v = self._align_series( + tuple(sub_indexer), value[item], multiindex_indexer + ) + else: + v = np.nan + + setter(item, v) + + # we have an equal len ndarray/convertible to our labels + # hasattr first, to avoid coercing to ndarray without reason. + # But we may be relying on the ndarray coercion to check ndim. + # Why not just convert to an ndarray earlier on if needed? + elif np.ndim(value) == 2: + + # note that this coerces the dtype if we are mixed + # GH 7551 + value = np.array(value, dtype=object) + if len(labels) != value.shape[1]: + raise ValueError( + "Must have equal len keys and value " + "when setting with an ndarray" + ) + + for i, item in enumerate(labels): + + # setting with a list, recoerces + setter(item, value[:, i].tolist()) + + # we have an equal len list/ndarray + elif _can_do_equal_len( + labels, value, plane_indexer, lplane_indexer, self.obj + ): + setter(labels[0], value) + + # per label values + else: + + if len(labels) != len(value): + raise ValueError( + "Must have equal len keys and value " + "when setting with an iterable" + ) + + for item, v in zip(labels, value): + setter(item, v) + else: + + # scalar + for item in labels: + setter(item, value) - # check that the key does not exceed the maximum size of the index - if len(arr) and (arr.max() >= len_axis or arr.min() < -len_axis): - raise IndexError("positional indexers are out-of-bounds") else: - raise ValueError(f"Can only index by location with a [{self._valid_types}]") + if isinstance(indexer, tuple): + indexer = maybe_convert_ix(*indexer) + + # if we are setting on the info axis ONLY + # set using those methods to avoid block-splitting + # logic here + if ( + len(indexer) > info_axis + and is_integer(indexer[info_axis]) + and all( + com.is_null_slice(idx) + for i, idx in enumerate(indexer) + if i != info_axis + ) + and item_labels.is_unique + ): + self.obj[item_labels[indexer[info_axis]]] = value + return - def _has_valid_setitem_indexer(self, indexer): - self._has_valid_positional_setitem_indexer(indexer) + if isinstance(value, (ABCSeries, dict)): + # TODO(EA): ExtensionBlock.setitem this causes issues with + # setting for extensionarrays that store dicts. Need to decide + # if it's worth supporting that. + value = self._align_series(indexer, Series(value)) - def _has_valid_positional_setitem_indexer(self, indexer) -> bool: + elif isinstance(value, ABCDataFrame): + value = self._align_frame(indexer, value) + + # check for chained assignment + self.obj._check_is_chained_assignment_possible() + + # actually do the set + self.obj._consolidate_inplace() + self.obj._data = self.obj._data.setitem(indexer=indexer, value=value) + self.obj._maybe_update_cacher(clear=True) + + def _setitem_with_indexer_missing(self, indexer, value): """ - Validate that a positional indexer cannot enlarge its target - will raise if needed, does not modify the indexer externally. + Insert new row(s) or column(s) into the Series or DataFrame. + """ + from pandas import Series + + # reindex the axis to the new value + # and set inplace + if self.ndim == 1: + index = self.obj.index + new_index = index.insert(len(index), indexer) + + # we have a coerced indexer, e.g. a float + # that matches in an Int64Index, so + # we will not create a duplicate index, rather + # index to that element + # e.g. 0.0 -> 0 + # GH#12246 + if index.is_unique: + new_indexer = index.get_indexer([new_index[-1]]) + if (new_indexer != -1).any(): + return self._setitem_with_indexer(new_indexer, value) + + # this preserves dtype of the value + new_values = Series([value])._values + if len(self.obj._values): + # GH#22717 handle casting compatibility that np.concatenate + # does incorrectly + new_values = concat_compat([self.obj._values, new_values]) + self.obj._data = self.obj._constructor( + new_values, index=new_index, name=self.obj.name + )._data + self.obj._maybe_update_cacher(clear=True) - Returns - ------- - bool - """ - if isinstance(indexer, dict): - raise IndexError(f"{self.name} cannot enlarge its target object") - else: - if not isinstance(indexer, tuple): - indexer = _tuplify(self.ndim, indexer) - for ax, i in zip(self.obj.axes, indexer): - if isinstance(i, slice): - # should check the stop slice? - pass - elif is_list_like_indexer(i): - # should check the elements? - pass - elif is_integer(i): - if i >= len(ax): - raise IndexError( - f"{self.name} cannot enlarge its target object" - ) - elif isinstance(i, dict): - raise IndexError(f"{self.name} cannot enlarge its target object") + elif self.ndim == 2: - return True + if not len(self.obj.columns): + # no columns and scalar + raise ValueError("cannot set a frame with no defined columns") - def _is_scalar_access(self, key: Tuple) -> bool: - """ - Returns - ------- - bool - """ - # this is a shortcut accessor to both .loc and .iloc - # that provide the equivalent access of .at and .iat - # a) avoid getting things via sections and (to minimize dtype changes) - # b) provide a performant path - if len(key) != self.ndim: - return False + if isinstance(value, ABCSeries): + # append a Series + value = value.reindex(index=self.obj.columns, copy=True) + value.name = indexer - for i, k in enumerate(key): - if not is_integer(k): - return False + else: + # a list-list + if is_list_like_indexer(value): + # must have conforming columns + if len(value) != len(self.obj.columns): + raise ValueError("cannot set a row with mismatched columns") - ax = self.obj.axes[i] - if not ax.is_unique: - return False + value = Series(value, index=self.obj.columns, name=indexer) - return True + self.obj._data = self.obj.append(value)._data + self.obj._maybe_update_cacher(clear=True) - def _validate_integer(self, key: int, axis: int) -> None: + def _align_series(self, indexer, ser: ABCSeries, multiindex_indexer: bool = False): """ - Check that 'key' is a valid position in the desired axis. - Parameters ---------- - key : int - Requested position. - axis : int - Desired axis. + indexer : tuple, slice, scalar + Indexer used to get the locations that will be set to `ser`. + ser : pd.Series + Values to assign to the locations specified by `indexer`. + multiindex_indexer : boolean, optional + Defaults to False. Should be set to True if `indexer` was from + a `pd.MultiIndex`, to avoid unnecessary broadcasting. - Raises - ------ - IndexError - If 'key' is not a valid position in axis 'axis'. + Returns + ------- + `np.array` of `ser` broadcast to the appropriate shape for assignment + to the locations selected by `indexer` """ - len_axis = len(self.obj._get_axis(axis)) - if key >= len_axis or key < -len_axis: - raise IndexError("single positional indexer is out-of-bounds") + if isinstance(indexer, (slice, np.ndarray, list, Index)): + indexer = tuple([indexer]) - # ------------------------------------------------------------------- + if isinstance(indexer, tuple): - def _getitem_tuple(self, tup: Tuple): + # flatten np.ndarray indexers + def ravel(i): + return i.ravel() if isinstance(i, np.ndarray) else i - self._has_valid_tuple(tup) - try: - return self._getitem_lowerdim(tup) - except IndexingError: - pass + indexer = tuple(map(ravel, indexer)) - retval = self.obj - axis = 0 - for i, key in enumerate(tup): - if com.is_null_slice(key): - axis += 1 - continue + aligners = [not com.is_null_slice(idx) for idx in indexer] + sum_aligners = sum(aligners) + single_aligner = sum_aligners == 1 + is_frame = self.ndim == 2 + obj = self.obj - retval = getattr(retval, self.name)._getitem_axis(key, axis=axis) + # are we a single alignable value on a non-primary + # dim (e.g. panel: 1,2, or frame: 0) ? + # hence need to align to a single axis dimension + # rather that find all valid dims - # if the dim was reduced, then pass a lower-dim the next time - if retval.ndim < self.ndim: - # TODO: this is never reached in tests; can we confirm that - # it is impossible? - axis -= 1 + # frame + if is_frame: + single_aligner = single_aligner and aligners[0] - # try to get for the next axis - axis += 1 + # we have a frame, with multiple indexers on both axes; and a + # series, so need to broadcast (see GH5206) + if sum_aligners == self.ndim and all(is_sequence(_) for _ in indexer): + ser = ser.reindex(obj.axes[0][indexer[0]], copy=True)._values - return retval + # single indexer + if len(indexer) > 1 and not multiindex_indexer: + len_indexer = len(indexer[1]) + ser = np.tile(ser, len_indexer).reshape(len_indexer, -1).T - def _get_list_axis(self, key, axis: int): - """ - Return Series values by list or array of integers. + return ser - Parameters - ---------- - key : list-like positional indexer - axis : int + for i, idx in enumerate(indexer): + ax = obj.axes[i] - Returns - ------- - Series object + # multiple aligners (or null slices) + if is_sequence(idx) or isinstance(idx, slice): + if single_aligner and com.is_null_slice(idx): + continue + new_ix = ax[idx] + if not is_list_like_indexer(new_ix): + new_ix = Index([new_ix]) + else: + new_ix = Index(new_ix) + if ser.index.equals(new_ix) or not len(new_ix): + return ser._values.copy() - Notes - ----- - `axis` can only be zero. - """ - try: - return self.obj._take_with_is_copy(key, axis=axis) - except IndexError: - # re-raise with different error message - raise IndexError("positional indexers are out-of-bounds") + return ser.reindex(new_ix)._values - def _getitem_axis(self, key, axis: int): - if isinstance(key, slice): - return self._get_slice_axis(key, axis=axis) + # 2 dims + elif single_aligner: - if isinstance(key, list): - key = np.asarray(key) + # reindex along index + ax = self.obj.axes[1] + if ser.index.equals(ax) or not len(ax): + return ser._values.copy() + return ser.reindex(ax)._values - if com.is_bool_indexer(key): - self._validate_key(key, axis) - return self._getbool_axis(key, axis=axis) + elif is_scalar(indexer): + ax = self.obj._get_axis(1) - # a list of integers - elif is_list_like_indexer(key): - return self._get_list_axis(key, axis=axis) + if ser.index.equals(ax): + return ser._values.copy() - # a single integer - else: - key = item_from_zerodim(key) - if not is_integer(key): - raise TypeError("Cannot index by location index with a non-integer key") + return ser.reindex(ax)._values - # validate the location - self._validate_integer(key, axis) + raise ValueError("Incompatible indexer with Series") - return self.obj._ixs(key, axis=axis) + def _align_frame(self, indexer, df: ABCDataFrame): + is_frame = self.ndim == 2 - def _get_slice_axis(self, slice_obj: slice, axis: int): - # caller is responsible for ensuring non-None axis - obj = self.obj + if isinstance(indexer, tuple): - if not need_slice(slice_obj): - return obj.copy(deep=False) + idx, cols = None, None + sindexers = [] + for i, ix in enumerate(indexer): + ax = self.obj.axes[i] + if is_sequence(ix) or isinstance(ix, slice): + if isinstance(ix, np.ndarray): + ix = ix.ravel() + if idx is None: + idx = ax[ix] + elif cols is None: + cols = ax[ix] + else: + break + else: + sindexers.append(i) - labels = obj._get_axis(axis) - indexer = labels._convert_slice_indexer(slice_obj, kind="iloc") - return self.obj._slice(indexer, axis=axis, kind="iloc") + if idx is not None and cols is not None: - def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): - """ - Much simpler as we only have to deal with our valid types. - """ - labels = self.obj._get_axis(axis) + if df.index.equals(idx) and df.columns.equals(cols): + val = df.copy()._values + else: + val = df.reindex(idx, columns=cols)._values + return val - # make need to convert a float key - if isinstance(key, slice): - return labels._convert_slice_indexer(key, kind="iloc") + elif (isinstance(indexer, slice) or is_list_like_indexer(indexer)) and is_frame: + ax = self.obj.index[indexer] + if df.index.equals(ax): + val = df.copy()._values + else: + + # we have a multi-index and are trying to align + # with a particular, level GH3738 + if ( + isinstance(ax, ABCMultiIndex) + and isinstance(df.index, ABCMultiIndex) + and ax.nlevels != df.index.nlevels + ): + raise TypeError( + "cannot align on a multi-index with out " + "specifying the join levels" + ) - elif is_float(key): - labels._validate_indexer("positional", key, "iloc") - return key + val = df.reindex(index=ax)._values + return val - self._validate_key(key, axis) - return key + raise ValueError("Incompatible indexer with DataFrame") class _ScalarAccessIndexer(_NDFrameIndexerBase): @@ -2207,10 +2133,12 @@ def check_bool_indexer(index: Index, key) -> np.ndarray: "the indexed object do not match)." ) result = result.astype(bool)._values - else: - # key might be sparse / object-dtype bool, check_array_indexer needs bool array + elif is_object_dtype(key): + # key might be object-dtype bool, check_array_indexer needs bool array result = np.asarray(result, dtype=bool) result = check_array_indexer(index, result) + else: + result = check_array_indexer(index, result) return result diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 85a26179276f5..34fa4c0e6544e 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -85,8 +85,6 @@ import pandas.core.missing as missing from pandas.core.nanops import nanpercentile -from pandas.io.formats.printing import pprint_thing - class Block(PandasObject): """ @@ -159,7 +157,8 @@ def _check_ndim(self, values, ndim): @property def _holder(self): - """The array-like that can hold the underlying values. + """ + The array-like that can hold the underlying values. None for 'Block', overridden by subclasses that don't use an ndarray. @@ -257,7 +256,8 @@ def mgr_locs(self, new_mgr_locs): @property def array_dtype(self): - """ the dtype to return if I want to construct this block as an + """ + the dtype to return if I want to construct this block as an array """ return self.dtype @@ -284,16 +284,11 @@ def __repr__(self) -> str: # don't want to print out all of the items here name = type(self).__name__ if self._is_single_block: - result = f"{name}: {len(self)} dtype: {self.dtype}" - else: - shape = " x ".join(pprint_thing(s) for s in self.shape) - result = ( - f"{name}: {pprint_thing(self.mgr_locs.indexer)}, " - f"{shape}, dtype: {self.dtype}" - ) + shape = " x ".join(str(s) for s in self.shape) + result = f"{name}: {self.mgr_locs.indexer}, {shape}, dtype: {self.dtype}" return result @@ -319,10 +314,7 @@ def getitem_block(self, slicer, new_mgr_locs=None): As of now, only supports slices that preserve dimensionality. """ if new_mgr_locs is None: - if isinstance(slicer, tuple): - axis0_slicer = slicer[0] - else: - axis0_slicer = slicer + axis0_slicer = slicer[0] if isinstance(slicer, tuple) else slicer new_mgr_locs = self.mgr_locs[axis0_slicer] new_values = self._slice(slicer) @@ -383,7 +375,8 @@ def delete(self, loc): self.mgr_locs = self.mgr_locs.delete(loc) def apply(self, func, **kwargs) -> List["Block"]: - """ apply the function to my values; return a block if we are not + """ + apply the function to my values; return a block if we are not one """ with np.errstate(all="ignore"): @@ -409,7 +402,8 @@ def _split_op_result(self, result) -> List["Block"]: return [result] def fillna(self, value, limit=None, inplace=False, downcast=None): - """ fillna on the block with the value. If we fail, then convert to + """ + fillna on the block with the value. If we fail, then convert to ObjectBlock and try again """ inplace = validate_bool_kwarg(inplace, "inplace") @@ -462,7 +456,6 @@ def split_and_operate(self, mask, f, inplace: bool): ------- list of blocks """ - if mask is None: mask = np.broadcast_to(True, shape=self.shape) @@ -519,7 +512,6 @@ def _maybe_downcast(self, blocks: List["Block"], downcast=None) -> List["Block"] def downcast(self, dtypes=None): """ try to downcast each item to the dict of dtypes if present """ - # turn it off completely if dtypes is False: return self @@ -659,11 +651,11 @@ def convert( timedelta: bool = True, coerce: bool = False, ): - """ attempt to coerce any object types to better types return a copy + """ + attempt to coerce any object types to better types return a copy of the block (if copy = True) by definition we are not an ObjectBlock here! """ - return self.copy() if copy else self def _can_hold_element(self, element: Any) -> bool: @@ -705,11 +697,11 @@ def copy(self, deep=True): def replace( self, to_replace, value, inplace=False, filter=None, regex=False, convert=True ): - """replace the to_replace value with value, possible to create new + """ + replace the to_replace value with value, possible to create new blocks here this is just a call to putmask. regex is not used here. It is used in ObjectBlocks. It is here for API compatibility. """ - inplace = validate_bool_kwarg(inplace, "inplace") original_to_replace = to_replace @@ -830,6 +822,9 @@ def setitem(self, indexer, value): """ transpose = self.ndim == 2 + if isinstance(indexer, np.ndarray) and indexer.ndim > self.ndim: + raise ValueError(f"Cannot set values with ndim > {self.ndim}") + # coerce None values, if appropriate if value is None: if self.is_numeric: @@ -923,7 +918,8 @@ def setitem(self, indexer, value): return block def putmask(self, mask, new, align=True, inplace=False, axis=0, transpose=False): - """ putmask the data to the block; it is possible that we may create a + """ + putmask the data to the block; it is possible that we may create a new dtype of block return the resulting block(s) @@ -942,7 +938,6 @@ def putmask(self, mask, new, align=True, inplace=False, axis=0, transpose=False) ------- a list of new blocks, the result of the putmask """ - new_values = self.values if inplace else self.values.copy() new = getattr(new, "values", new) @@ -1052,7 +1047,6 @@ def coerce_to_target_dtype(self, other): we can also safely try to coerce to the same dtype and will receive the same block """ - # if we cannot then coerce to object dtype, _ = infer_dtype_from(other, pandas_dtype=True) @@ -1185,7 +1179,6 @@ def _interpolate_with_fill( downcast=None, ): """ fillna but using the interpolate machinery """ - inplace = validate_bool_kwarg(inplace, "inplace") # if we are coercing, then don't force the conversion @@ -1229,7 +1222,6 @@ def _interpolate( **kwargs, ): """ interpolate using scipy wrappers """ - inplace = validate_bool_kwarg(inplace, "inplace") data = self.values if inplace else self.values.copy() @@ -1277,7 +1269,6 @@ def take_nd(self, indexer, axis, new_mgr_locs=None, fill_tuple=None): Take values according to indexer and return them as a block.bb """ - # algos.take_nd dispatches for DatetimeTZBlock, CategoricalBlock # so need to preserve types # sparse is treated like an ndarray, but needs .get_values() shaping @@ -1316,7 +1307,6 @@ def diff(self, n: int, axis: int = 1) -> List["Block"]: def shift(self, periods, axis=0, fill_value=None): """ shift the block by periods, possibly upcast """ - # convert integer to float if necessary. need to do a lot more than # that, handle boolean etc also new_values, fill_value = maybe_upcast(self.values, fill_value) @@ -1462,7 +1452,8 @@ def equals(self, other) -> bool: return array_equivalent(self.values, other.values) def _unstack(self, unstacker_func, new_columns, n_rows, fill_value): - """Return a list of unstacked blocks of self + """ + Return a list of unstacked blocks of self Parameters ---------- @@ -1576,7 +1567,6 @@ def _replace_coerce( ------- A new block if there is anything to replace or the original block. """ - if mask.any(): if not regex: self = self.coerce_to_target_dtype(value) @@ -1601,7 +1591,8 @@ class NonConsolidatableMixIn: _validate_ndim = False def __init__(self, values, placement, ndim=None): - """Initialize a non-consolidatable block. + """ + Initialize a non-consolidatable block. 'ndim' may be inferred from 'placement'. @@ -1716,7 +1707,8 @@ def _get_unstack_items(self, unstacker, new_columns): class ExtensionBlock(NonConsolidatableMixIn, Block): - """Block for holding extension types. + """ + Block for holding extension types. Notes ----- @@ -1774,7 +1766,8 @@ def is_numeric(self): return self.values.dtype._is_numeric def setitem(self, indexer, value): - """Set the value inplace, returning a same-typed block. + """ + Set the value inplace, returning a same-typed block. This differs from Block.setitem by not allowing setitem to change the dtype of the Block. @@ -1858,7 +1851,6 @@ def _can_hold_element(self, element: Any) -> bool: def _slice(self, slicer): """ return a slice of my values """ - # slice the category # return same dims as we currently have @@ -2061,7 +2053,6 @@ def to_native_types( **kwargs, ): """ convert to our native types format, slicing if desired """ - values = self.values if slicer is not None: values = values[:, slicer] @@ -2248,7 +2239,6 @@ def to_native_types( self, slicer=None, na_rep=None, date_format=None, quoting=None, **kwargs ): """ convert to our native types format, slicing if desired """ - values = self.values i8values = self.values.view("i8") @@ -2311,7 +2301,8 @@ def _holder(self): return DatetimeArray def _maybe_coerce_values(self, values): - """Input validation for values passed to __init__. Ensure that + """ + Input validation for values passed to __init__. Ensure that we have datetime64TZ, coercing if necessary. Parameters @@ -2526,7 +2517,6 @@ def should_store(self, value): def to_native_types(self, slicer=None, na_rep=None, quoting=None, **kwargs): """ convert to our native types format, slicing if desired """ - values = self.values if slicer is not None: values = values[:, slicer] @@ -2601,7 +2591,8 @@ def __init__(self, values, placement=None, ndim=2): @property def is_bool(self): - """ we can be a bool if we have only bool values but are of type + """ + we can be a bool if we have only bool values but are of type object """ return lib.is_bool_array(self.values.ravel()) @@ -2614,12 +2605,12 @@ def convert( timedelta: bool = True, coerce: bool = False, ): - """ attempt to coerce any object types to better types return a copy of + """ + attempt to coerce any object types to better types return a copy of the block (if copy = True) by definition we ARE an ObjectBlock!!!!! can return multiple blocks! """ - # operate column-by-column def f(mask, val, idx): shape = val.shape @@ -2908,7 +2899,8 @@ def _holder(self): @property def array_dtype(self): - """ the dtype to return if I want to construct this block as an + """ + the dtype to return if I want to construct this block as an array """ return np.object_ @@ -2921,7 +2913,6 @@ def to_dense(self): def to_native_types(self, slicer=None, na_rep="", quoting=None, **kwargs): """ convert to our native types format, slicing if desired """ - values = self.values if slicer is not None: # Categorical is always one dimension @@ -3057,7 +3048,6 @@ def make_block(values, placement, klass=None, ndim=None, dtype=None): def _extend_blocks(result, blocks=None): """ return a new extended blocks, given the result """ - if blocks is None: blocks = [] if isinstance(result, list): @@ -3153,7 +3143,6 @@ def _putmask_smart(v, mask, n): -------- ndarray.putmask """ - # we cannot use np.asarray() here as we cannot have conversions # that numpy does when numeric are mixed with strings diff --git a/pandas/core/internals/concat.py b/pandas/core/internals/concat.py index c75373b82305c..515e1bcd761b6 100644 --- a/pandas/core/internals/concat.py +++ b/pandas/core/internals/concat.py @@ -204,10 +204,9 @@ def get_reindexed_values(self, empty_dtype, upcasted_na): missing_arr.fill(fill_value) return missing_arr - if not self.indexers: - if not self.block._can_consolidate: - # preserve these for validation in concat_compat - return self.block.values + if (not self.indexers) and (not self.block._can_consolidate): + # preserve these for validation in concat_compat + return self.block.values if self.block.is_bool and not self.block.is_categorical: # External code requested filling/upcasting, bool values must @@ -372,7 +371,7 @@ def _get_empty_dtype_and_na(join_units): raise AssertionError(msg) -def is_uniform_join_units(join_units): +def is_uniform_join_units(join_units) -> bool: """ Check if the join units consist of blocks of uniform type that can be concatenated using Block.concat_same_type instead of the generic @@ -409,7 +408,6 @@ def _trim_join_unit(join_unit, length): Extra items that didn't fit are returned as a separate block. """ - if 0 not in join_unit.indexers: extra_indexers = join_unit.indexers diff --git a/pandas/core/internals/construction.py b/pandas/core/internals/construction.py index 798386825d802..57ed2555761be 100644 --- a/pandas/core/internals/construction.py +++ b/pandas/core/internals/construction.py @@ -78,7 +78,6 @@ def masked_rec_array_to_mgr(data, index, columns, dtype, copy: bool): """ Extract from a masked rec array and create the manager. """ - # essentially process a record array then fill it fill_value = data.fill_value fdata = ma.getdata(data) @@ -535,7 +534,8 @@ def _list_of_series_to_arrays(data, columns, coerce_float=False, dtype=None): def _list_of_dict_to_arrays(data, columns, coerce_float=False, dtype=None): - """Convert list of dicts to numpy arrays + """ + Convert list of dicts to numpy arrays if `columns` is not passed, column names are inferred from the records - for OrderedDict and dicts, the column names match @@ -555,7 +555,6 @@ def _list_of_dict_to_arrays(data, columns, coerce_float=False, dtype=None): tuple arrays, columns """ - if columns is None: gen = (list(x.keys()) for x in data) sort = not any(isinstance(d, dict) for d in data) @@ -603,7 +602,6 @@ def sanitize_index(data, index: Index): Sanitize an index type to return an ndarray of the underlying, pass through a non-Index. """ - if len(data) != len(index): raise ValueError("Length of values does not match length of index") diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index 08ae0b02169d4..329bfdf543c62 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -8,6 +8,7 @@ import numpy as np from pandas._libs import Timedelta, Timestamp, internals as libinternals, lib +from pandas._typing import DtypeObj from pandas.util._validators import validate_bool_kwarg from pandas.core.dtypes.cast import ( @@ -101,7 +102,9 @@ class BlockManager(PandasObject): Parameters ---------- - + blocks: Sequence of Block + axes: Sequence of Index + do_integrity_check: bool, default True Notes ----- @@ -138,7 +141,7 @@ def __init__( if do_integrity_check: self._verify_integrity() - self._consolidate_check() + self._known_consolidated = False self._rebuild_blknos_and_blklocs() @@ -357,7 +360,6 @@ def apply(self, f, filter=None, **kwargs): ------- BlockManager """ - result_blocks = [] # filter kwarg is used in replace-* family of methods @@ -453,7 +455,6 @@ def quantile( ------- Block Manager (new object) """ - # Series dispatches to DataFrame for quantile, which allows us to # simplify some of the code here and in the blocks assert self.ndim >= 2 @@ -569,7 +570,6 @@ def replace(self, value, **kwargs): def replace_list(self, src_list, dest_list, inplace=False, regex=False): """ do a list replace """ - inplace = validate_bool_kwarg(inplace, "inplace") # figure out our mask a-priori to avoid repeated replacements @@ -589,7 +589,7 @@ def comp(s, regex=False): ) return _compare_or_regex_search(values, s, regex) - masks = [comp(s, regex) for i, s in enumerate(src_list)] + masks = [comp(s, regex) for s in src_list] result_blocks = [] src_len = len(src_list) - 1 @@ -726,7 +726,6 @@ def get_slice(self, slobj: slice, axis: int = 0): new_axes[axis] = new_axes[axis][slobj] bm = type(self)(new_blocks, new_axes, do_integrity_check=False) - bm._consolidate_inplace() return bm def __contains__(self, item) -> bool: @@ -755,10 +754,7 @@ def copy(self, deep=True): # hit in e.g. tests.io.json.test_pandas def copy_func(ax): - if deep == "all": - return ax.copy(deep=True) - else: - return ax.view() + return ax.copy(deep=True) if deep == "all" else ax.view() new_axes = [copy_func(ax) for ax in self.axes] else: @@ -851,7 +847,7 @@ def to_dict(self, copy: bool = True): return {dtype: self.combine(blocks, copy=copy) for dtype, blocks in bd.items()} - def fast_xs(self, loc): + def fast_xs(self, loc: int): """ get a cross sectional for a given location in the items ; handle dups @@ -887,12 +883,12 @@ def fast_xs(self, loc): for i, rl in enumerate(blk.mgr_locs): result[rl] = blk.iget((i, loc)) - if is_extension_array_dtype(dtype): + if isinstance(dtype, ExtensionDtype): result = dtype.construct_array_type()._from_sequence(result, dtype=dtype) return result - def consolidate(self): + def consolidate(self) -> "BlockManager": """ Join together blocks having same dtype @@ -944,7 +940,7 @@ def get(self, item): new_axis=self.items[indexer], indexer=indexer, axis=0, allow_dups=True ) - def iget(self, i): + def iget(self, i: int) -> "SingleBlockManager": """ Return the data as a SingleBlockManager. """ @@ -1246,7 +1242,6 @@ def _slice_take_blocks_ax0(self, slice_or_indexer, fill_tuple=None): ------- new_blocks : list of Block """ - allow_fill = fill_tuple is not None sl_type, slobj, sllen = _preprocess_slice_or_indexer( @@ -1323,7 +1318,6 @@ def _slice_take_blocks_ax0(self, slice_or_indexer, fill_tuple=None): return blocks def _make_na_block(self, placement, fill_value=None): - # TODO: infer dtypes other than float64 from fill_value if fill_value is None: fill_value = np.nan @@ -1383,7 +1377,7 @@ def canonicalize(block): block.equals(oblock) for block, oblock in zip(self_blocks, other_blocks) ) - def unstack(self, unstacker_func, fill_value): + def unstack(self, unstacker_func, fill_value) -> "BlockManager": """ Return a BlockManager with all blocks unstacked.. @@ -1402,8 +1396,8 @@ def unstack(self, unstacker_func, fill_value): dummy = unstacker_func(np.empty((0, 0)), value_columns=self.items) new_columns = dummy.get_new_columns() new_index = dummy.get_new_index() - new_blocks = [] - columns_mask = [] + new_blocks: List[Block] = [] + columns_mask: List[np.ndarray] = [] for blk in self.blocks: blocks, mask = blk._unstack( @@ -1484,7 +1478,7 @@ def _post_setstate(self): pass @property - def _block(self): + def _block(self) -> Block: return self.blocks[0] @property @@ -1501,14 +1495,14 @@ def _blklocs(self): """ compat with BlockManager """ return None - def get_slice(self, slobj, axis=0): + def get_slice(self, slobj: slice, axis: int = 0) -> "SingleBlockManager": if axis >= self.ndim: raise IndexError("Requested axis not found in manager") - return type(self)(self._block._slice(slobj), self.index[slobj], fastpath=True,) + return type(self)(self._block._slice(slobj), self.index[slobj], fastpath=True) @property - def index(self): + def index(self) -> Index: return self.axes[0] @property @@ -1522,7 +1516,7 @@ def array_dtype(self): def get_dtype_counts(self): return {self.dtype.name: 1} - def get_dtypes(self): + def get_dtypes(self) -> np.ndarray: return np.array([self._block.dtype]) def external_values(self): @@ -1533,7 +1527,7 @@ def internal_values(self): """The array that Series._values returns""" return self._block.internal_values() - def get_values(self): + def get_values(self) -> np.ndarray: """ return a dense type view """ return np.array(self._block.to_dense(), copy=False) @@ -1541,7 +1535,7 @@ def get_values(self): def _can_hold_na(self) -> bool: return self._block._can_hold_na - def is_consolidated(self): + def is_consolidated(self) -> bool: return True def _consolidate_check(self): @@ -1762,7 +1756,8 @@ def form_blocks(arrays, names, axes): def _simple_blockify(tuples, dtype): - """ return a single array of a block that has a single dtype; if dtype is + """ + return a single array of a block that has a single dtype; if dtype is not None, coerce to this dtype """ values, placement = _stack_arrays(tuples, dtype) @@ -1777,7 +1772,6 @@ def _simple_blockify(tuples, dtype): def _multi_blockify(tuples, dtype=None): """ return an array of blocks that potentially have different dtypes """ - # group by dtype grouper = itertools.groupby(tuples, lambda x: x[2].dtype) @@ -1819,10 +1813,9 @@ def _shape_compat(x): return stacked, placement -def _interleaved_dtype( - blocks: List[Block], -) -> Optional[Union[np.dtype, ExtensionDtype]]: - """Find the common dtype for `blocks`. +def _interleaved_dtype(blocks: Sequence[Block]) -> Optional[DtypeObj]: + """ + Find the common dtype for `blocks`. Parameters ---------- @@ -1830,7 +1823,7 @@ def _interleaved_dtype( Returns ------- - dtype : Optional[Union[np.dtype, ExtensionDtype]] + dtype : np.dtype, ExtensionDtype, or None None is returned when `blocks` is empty. """ if not len(blocks): @@ -1843,7 +1836,6 @@ def _consolidate(blocks): """ Merge blocks having same dtype, exclude non-consolidating blocks """ - # sort by _can_consolidate, dtype gkey = lambda x: x._consolidate_key grouper = itertools.groupby(sorted(blocks, key=gkey), gkey) diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py index 2bf2be082f639..a5c609473760d 100644 --- a/pandas/core/nanops.py +++ b/pandas/core/nanops.py @@ -30,7 +30,6 @@ is_timedelta64_dtype, pandas_dtype, ) -from pandas.core.dtypes.dtypes import DatetimeTZDtype from pandas.core.dtypes.missing import isna, na_value_for_dtype, notna bn = import_optional_dependency("bottleneck", raise_on_missing=False, on_version="warn") @@ -224,7 +223,6 @@ def _maybe_get_mask( ------- Optional[np.ndarray] """ - if mask is None: if is_bool_dtype(values.dtype) or is_integer_dtype(values.dtype): # Boolean data cannot contain nulls, so signal via mask being None @@ -279,7 +277,6 @@ def _get_values( fill_value : Any fill value used """ - # In _get_values is only called from within nanops, and in all cases # with scalar fill_value. This guarantee is important for the # maybe_upcast_putmask call below @@ -338,7 +335,6 @@ def _na_ok_dtype(dtype) -> bool: def _wrap_results(result, dtype: Dtype, fill_value=None): """ wrap our results if needed """ - if is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype): if fill_value is None: # GH#24293 @@ -519,7 +515,6 @@ def nansum( return _wrap_results(the_sum, dtype) -@disallow("M8", DatetimeTZDtype) @bottleneck_switch() def nanmean(values, axis=None, skipna=True, mask=None): """ @@ -577,7 +572,6 @@ def nanmean(values, axis=None, skipna=True, mask=None): return _wrap_results(the_mean, dtype) -@disallow("M8") @bottleneck_switch() def nanmedian(values, axis=None, skipna=True, mask=None): """ @@ -610,8 +604,12 @@ def get_median(x): return np.nanmedian(x[mask]) values, mask, dtype, dtype_max, _ = _get_values(values, skipna, mask=mask) - if not is_float_dtype(values): - values = values.astype("f8") + if not is_float_dtype(values.dtype): + try: + values = values.astype("f8") + except ValueError: + # e.g. "could not convert string to float: 'a'" + raise TypeError if mask is not None: values[mask] = np.nan @@ -654,7 +652,8 @@ def _get_counts_nanvar( ddof: int, dtype: Dtype = float, ) -> Tuple[Union[int, np.ndarray], Union[int, np.ndarray]]: - """ Get the count of non-null values along an axis, accounting + """ + Get the count of non-null values along an axis, accounting for degrees of freedom. Parameters @@ -833,7 +832,6 @@ def nansem( >>> nanops.nansem(s) 0.5773502691896258 """ - # This checks if non-numeric-like data is passed with numeric_only=False # and raises a TypeError otherwise nanvar(values, axis, skipna, ddof=ddof, mask=mask) @@ -959,7 +957,8 @@ def nanskew( skipna: bool = True, mask: Optional[np.ndarray] = None, ) -> float: - """ Compute the sample skewness. + """ + Compute the sample skewness. The statistic computed here is the adjusted Fisher-Pearson standardized moment coefficient G1. The algorithm computes this coefficient directly @@ -1197,7 +1196,8 @@ def _get_counts( axis: Optional[int], dtype: Dtype = float, ) -> Union[int, np.ndarray]: - """ Get the count of non-null values along an axis + """ + Get the count of non-null values along an axis Parameters ---------- @@ -1359,7 +1359,11 @@ def _ensure_numeric(x): try: x = x.astype(np.complex128) except (TypeError, ValueError): - x = x.astype(np.float64) + try: + x = x.astype(np.float64) + except ValueError: + # GH#29941 we get here with object arrays containing strs + raise TypeError(f"Could not convert {x} to numeric") else: if not np.any(np.imag(x)): x = x.real diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 0312c11a6d590..d0adf2da04db3 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -5,22 +5,17 @@ """ import datetime import operator -from typing import Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Optional, Set, Tuple import numpy as np from pandas._libs import Timedelta, Timestamp, lib from pandas._libs.ops_dispatch import maybe_dispatch_ufunc_to_dunder_op # noqa:F401 -from pandas._typing import Level +from pandas._typing import ArrayLike, Level from pandas.util._decorators import Appender from pandas.core.dtypes.common import is_list_like, is_timedelta64_dtype -from pandas.core.dtypes.generic import ( - ABCDataFrame, - ABCExtensionArray, - ABCIndexClass, - ABCSeries, -) +from pandas.core.dtypes.generic import ABCDataFrame, ABCIndexClass, ABCSeries from pandas.core.dtypes.missing import isna from pandas.core.construction import extract_array @@ -61,6 +56,9 @@ rxor, ) +if TYPE_CHECKING: + from pandas import DataFrame # noqa:F401 + # ----------------------------------------------------------------------------- # constants ARITHMETIC_BINOPS: Set[str] = { @@ -254,7 +252,6 @@ def _get_opstr(op): ------- op_str : string or None """ - return { operator.add: "+", radd: "+", @@ -430,7 +427,6 @@ def column_op(a, b): def _align_method_SERIES(left, right, align_asobject=False): """ align lhs and rhs Series """ - # ToDo: Different from _align_method_FRAME, list, tuple and ndarray # are not coerced here # because Series has inconsistencies described in #13637 @@ -450,10 +446,7 @@ def _align_method_SERIES(left, right, align_asobject=False): def _construct_result( - left: ABCSeries, - result: Union[np.ndarray, ABCExtensionArray], - index: ABCIndexClass, - name, + left: ABCSeries, result: ArrayLike, index: ABCIndexClass, name, ): """ Construct an appropriately-labelled Series from the result of an op. @@ -517,6 +510,7 @@ def _comp_method_SERIES(cls, op, special): Wrapper function for Series arithmetic operations, to avoid code duplication. """ + str_rep = _get_opstr(op) op_name = _get_op_name(op, special) @unpack_zerodim_and_defer(op_name) @@ -530,7 +524,7 @@ def wrapper(self, other): lvalues = extract_array(self, extract_numpy=True) rvalues = extract_array(other, extract_numpy=True) - res_values = comparison_op(lvalues, rvalues, op) + res_values = comparison_op(lvalues, rvalues, op, str_rep) return _construct_result(self, res_values, index=self.index, name=res_name) @@ -705,6 +699,58 @@ def to_series(right): return left, right +def _should_reindex_frame_op( + left: "DataFrame", right, axis, default_axis: int, fill_value, level +) -> bool: + """ + Check if this is an operation between DataFrames that will need to reindex. + """ + assert isinstance(left, ABCDataFrame) + + if not isinstance(right, ABCDataFrame): + return False + + if fill_value is None and level is None and axis is default_axis: + # TODO: any other cases we should handle here? + cols = left.columns.intersection(right.columns) + if not (cols.equals(left.columns) and cols.equals(right.columns)): + return True + + return False + + +def _frame_arith_method_with_reindex( + left: "DataFrame", right: "DataFrame", op +) -> "DataFrame": + """ + For DataFrame-with-DataFrame operations that require reindexing, + operate only on shared columns, then reindex. + + Parameters + ---------- + left : DataFrame + right : DataFrame + op : binary operator + + Returns + ------- + DataFrame + """ + # GH#31623, only operate on shared columns + cols = left.columns.intersection(right.columns) + + new_left = left[cols] + new_right = right[cols] + result = op(new_left, new_right) + + # Do the join on the columns instead of using _align_method_FRAME + # to avoid constructing two potentially large/sparse DataFrames + join_columns, _, _ = left.columns.join( + right.columns, how="outer", level=None, return_indexers=True + ) + return result.reindex(join_columns, axis=1) + + def _arith_method_FRAME(cls, op, special): str_rep = _get_opstr(op) op_name = _get_op_name(op, special) @@ -722,6 +768,9 @@ def _arith_method_FRAME(cls, op, special): @Appender(doc) def f(self, other, axis=default_axis, level=None, fill_value=None): + if _should_reindex_frame_op(self, other, axis, default_axis, fill_value, level): + return _frame_arith_method_with_reindex(self, other, op) + self, other = _align_method_FRAME(self, other, axis, flex=True, level=level) if isinstance(other, ABCDataFrame): @@ -780,7 +829,7 @@ def f(self, other, axis=default_axis, level=None): return _combine_series_frame(self, other, op, axis=axis) else: # in this case we always have `np.ndim(other) == 0` - new_data = dispatch_to_series(self, other, op) + new_data = dispatch_to_series(self, other, op, str_rep) return self._construct_result(new_data) f.__name__ = op_name @@ -804,13 +853,15 @@ def f(self, other): new_data = dispatch_to_series(self, other, op, str_rep) elif isinstance(other, ABCSeries): - new_data = dispatch_to_series(self, other, op, axis="columns") + new_data = dispatch_to_series( + self, other, op, str_rep=str_rep, axis="columns" + ) else: # straight boolean comparisons we want to allow all columns # (regardless of dtype to pass thru) See #4537 for discussion. - new_data = dispatch_to_series(self, other, op) + new_data = dispatch_to_series(self, other, op, str_rep) return self._construct_result(new_data) diff --git a/pandas/core/ops/array_ops.py b/pandas/core/ops/array_ops.py index 3302ed9c219e6..b216a927f65b3 100644 --- a/pandas/core/ops/array_ops.py +++ b/pandas/core/ops/array_ops.py @@ -4,11 +4,12 @@ """ from functools import partial import operator -from typing import Any, Optional, Union +from typing import Any, Optional import numpy as np from pandas._libs import Timedelta, Timestamp, lib, ops as libops +from pandas._typing import ArrayLike from pandas.core.dtypes.cast import ( construct_1d_object_array_from_listlike, @@ -43,9 +44,9 @@ def comp_method_OBJECT_ARRAY(op, x, y): if isinstance(y, list): y = construct_1d_object_array_from_listlike(y) - # TODO: Should the checks below be ABCIndexClass? if isinstance(y, (np.ndarray, ABCSeries, ABCIndex)): - # TODO: should this be ABCIndexClass?? + # Note: these checks can be for ABCIndex and not ABCIndexClass + # because that is the only object-dtype class. if not is_object_dtype(y.dtype): y = y.astype(np.object_) @@ -125,7 +126,7 @@ def na_op(x, y): return na_op -def na_arithmetic_op(left, right, op, str_rep: str): +def na_arithmetic_op(left, right, op, str_rep: Optional[str], is_cmp: bool = False): """ Return the result of evaluating op on the passed in values. @@ -136,6 +137,8 @@ def na_arithmetic_op(left, right, op, str_rep: str): left : np.ndarray right : np.ndarray or scalar str_rep : str or None + is_cmp : bool, default False + If this a comparison operation. Returns ------- @@ -150,14 +153,22 @@ def na_arithmetic_op(left, right, op, str_rep: str): try: result = expressions.evaluate(op, str_rep, left, right) except TypeError: + if is_cmp: + # numexpr failed on comparison op, e.g. ndarray[float] > datetime + # In this case we do not fall back to the masked op, as that + # will handle complex numbers incorrectly, see GH#32047 + raise result = masked_arith_op(left, right, op) + if is_cmp and (is_scalar(result) or result is NotImplemented): + # numpy returned a scalar instead of operating element-wise + # e.g. numeric array vs str + return invalid_comparison(left, right, op) + return missing.dispatch_fill_zeros(op, left, right, result) -def arithmetic_op( - left: Union[np.ndarray, ABCExtensionArray], right: Any, op, str_rep: str -): +def arithmetic_op(left: ArrayLike, right: Any, op, str_rep: str): """ Evaluate an arithmetic operation `+`, `-`, `*`, `/`, `//`, `%`, `**`, ... @@ -175,7 +186,6 @@ def arithmetic_op( ndarrray or ExtensionArray Or a 2-tuple of these in the case of divmod or rdivmod. """ - from pandas.core.ops import maybe_upcast_for_op # NB: We assume that extract_array has already been called @@ -202,8 +212,8 @@ def arithmetic_op( def comparison_op( - left: Union[np.ndarray, ABCExtensionArray], right: Any, op -) -> Union[np.ndarray, ABCExtensionArray]: + left: ArrayLike, right: Any, op, str_rep: Optional[str] = None, +) -> ArrayLike: """ Evaluate a comparison operation `=`, `!=`, `>=`, `>`, `<=`, or `<`. @@ -216,9 +226,8 @@ def comparison_op( Returns ------- - ndarrray or ExtensionArray + ndarray or ExtensionArray """ - # NB: We assume extract_array has already been called on left and right lvalues = left rvalues = right @@ -249,16 +258,8 @@ def comparison_op( res_values = comp_method_OBJECT_ARRAY(op, lvalues, rvalues) else: - op_name = f"__{op.__name__}__" - method = getattr(lvalues, op_name) with np.errstate(all="ignore"): - res_values = method(rvalues) - - if res_values is NotImplemented: - res_values = invalid_comparison(lvalues, rvalues, op) - if is_scalar(res_values): - typ = type(rvalues) - raise TypeError(f"Could not compare {typ} type with Series") + res_values = na_arithmetic_op(lvalues, rvalues, op, str_rep, is_cmp=True) return res_values @@ -304,9 +305,7 @@ def na_logical_op(x: np.ndarray, y, op): return result.reshape(x.shape) -def logical_op( - left: Union[np.ndarray, ABCExtensionArray], right: Any, op -) -> Union[np.ndarray, ABCExtensionArray]: +def logical_op(left: ArrayLike, right: Any, op) -> ArrayLike: """ Evaluate a logical operation `|`, `&`, or `^`. @@ -322,7 +321,6 @@ def logical_op( ------- ndarrray or ExtensionArray """ - fill_int = lambda x: x def fill_bool(x, left=None): @@ -388,7 +386,7 @@ def get_array_op(op, str_rep: Optional[str] = None): """ op_name = op.__name__.strip("_") if op_name in {"eq", "ne", "lt", "le", "gt", "ge"}: - return partial(comparison_op, op=op) + return partial(comparison_op, op=op, str_rep=str_rep) elif op_name in {"and", "or", "xor", "rand", "ror", "rxor"}: return partial(logical_op, op=op) else: diff --git a/pandas/core/ops/common.py b/pandas/core/ops/common.py index f4b16cf4a0cf2..5c83591b0e71e 100644 --- a/pandas/core/ops/common.py +++ b/pandas/core/ops/common.py @@ -43,7 +43,6 @@ def _unpack_zerodim_and_defer(method, name: str): ------- method """ - is_cmp = name.strip("__") in {"eq", "ne", "lt", "le", "gt", "ge"} @wraps(method) diff --git a/pandas/core/ops/docstrings.py b/pandas/core/ops/docstrings.py index e3db65f11a332..203ea3946d1b2 100644 --- a/pandas/core/ops/docstrings.py +++ b/pandas/core/ops/docstrings.py @@ -34,6 +34,7 @@ def _make_flex_doc(op_name, typ): op_name=op_name, equiv=equiv, reverse=op_desc["reverse"], + series_returns=op_desc["series_returns"], ) if op_desc["series_examples"]: doc = doc_no_examples + op_desc["series_examples"] @@ -233,6 +234,10 @@ def _make_flex_doc(op_name, typ): dtype: float64 """ +_returns_series = """Series\n The result of the operation.""" + +_returns_tuple = """2-Tuple of Series\n The result of the operation.""" + _op_descriptions: Dict[str, Dict[str, Optional[str]]] = { # Arithmetic Operators "add": { @@ -240,18 +245,21 @@ def _make_flex_doc(op_name, typ): "desc": "Addition", "reverse": "radd", "series_examples": _add_example_SERIES, + "series_returns": _returns_series, }, "sub": { "op": "-", "desc": "Subtraction", "reverse": "rsub", "series_examples": _sub_example_SERIES, + "series_returns": _returns_series, }, "mul": { "op": "*", "desc": "Multiplication", "reverse": "rmul", "series_examples": _mul_example_SERIES, + "series_returns": _returns_series, "df_examples": None, }, "mod": { @@ -259,12 +267,14 @@ def _make_flex_doc(op_name, typ): "desc": "Modulo", "reverse": "rmod", "series_examples": _mod_example_SERIES, + "series_returns": _returns_series, }, "pow": { "op": "**", "desc": "Exponential power", "reverse": "rpow", "series_examples": _pow_example_SERIES, + "series_returns": _returns_series, "df_examples": None, }, "truediv": { @@ -272,6 +282,7 @@ def _make_flex_doc(op_name, typ): "desc": "Floating division", "reverse": "rtruediv", "series_examples": _div_example_SERIES, + "series_returns": _returns_series, "df_examples": None, }, "floordiv": { @@ -279,6 +290,7 @@ def _make_flex_doc(op_name, typ): "desc": "Integer division", "reverse": "rfloordiv", "series_examples": _floordiv_example_SERIES, + "series_returns": _returns_series, "df_examples": None, }, "divmod": { @@ -286,29 +298,51 @@ def _make_flex_doc(op_name, typ): "desc": "Integer division and modulo", "reverse": "rdivmod", "series_examples": None, + "series_returns": _returns_tuple, "df_examples": None, }, # Comparison Operators - "eq": {"op": "==", "desc": "Equal to", "reverse": None, "series_examples": None}, + "eq": { + "op": "==", + "desc": "Equal to", + "reverse": None, + "series_examples": None, + "series_returns": _returns_series, + }, "ne": { "op": "!=", "desc": "Not equal to", "reverse": None, "series_examples": None, + "series_returns": _returns_series, + }, + "lt": { + "op": "<", + "desc": "Less than", + "reverse": None, + "series_examples": None, + "series_returns": _returns_series, }, - "lt": {"op": "<", "desc": "Less than", "reverse": None, "series_examples": None}, "le": { "op": "<=", "desc": "Less than or equal to", "reverse": None, "series_examples": None, + "series_returns": _returns_series, + }, + "gt": { + "op": ">", + "desc": "Greater than", + "reverse": None, + "series_examples": None, + "series_returns": _returns_series, }, - "gt": {"op": ">", "desc": "Greater than", "reverse": None, "series_examples": None}, "ge": { "op": ">=", "desc": "Greater than or equal to", "reverse": None, "series_examples": None, + "series_returns": _returns_series, }, } @@ -339,8 +373,7 @@ def _make_flex_doc(op_name, typ): Returns ------- -Series - The result of the operation. +{series_returns} See Also -------- diff --git a/pandas/core/resample.py b/pandas/core/resample.py index fb837409a00f5..f19a82ab6f86a 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -23,6 +23,7 @@ from pandas.core.groupby.groupby import GroupBy, _GroupBy, _pipe_template, get_groupby from pandas.core.groupby.grouper import Grouper from pandas.core.groupby.ops import BinGrouper +from pandas.core.indexes.api import Index from pandas.core.indexes.datetimes import DatetimeIndex, date_range from pandas.core.indexes.period import PeriodIndex, period_range from pandas.core.indexes.timedeltas import TimedeltaIndex, timedelta_range @@ -182,7 +183,6 @@ def _get_binner(self): Create the BinGrouper, assume that self.set_grouper(obj) has already been called. """ - binner, bins, binlabels = self._get_binner_for_time() assert len(bins) == len(binlabels) bin_grouper = BinGrouper(bins, binlabels, indexer=self.groupby.indexer) @@ -344,7 +344,6 @@ def _groupby_and_aggregate(self, how, grouper=None, *args, **kwargs): """ Re-evaluate the obj with a groupby aggregation. """ - if grouper is None: self._set_binner() grouper = self.grouper @@ -396,7 +395,6 @@ def _apply_loffset(self, result): result : Series or DataFrame the result of resample """ - needs_offset = ( isinstance(self.loffset, (DateOffset, timedelta, np.timedelta64)) and isinstance(result.index, DatetimeIndex) @@ -424,10 +422,7 @@ def _wrap_result(self, result): if isinstance(result, ABCSeries) and result.empty: obj = self.obj - if isinstance(obj.index, PeriodIndex): - result.index = obj.index.asfreq(self.freq) - else: - result.index = obj.index._shallow_copy(freq=self.freq) + result.index = _asfreq_compat(obj.index, freq=self.freq) result.name = getattr(obj, "name", None) return result @@ -555,7 +550,6 @@ def backfill(self, limit=None): Examples -------- - Resampling a Series: >>> s = pd.Series([1, 2, 3], @@ -1160,7 +1154,6 @@ def _downsample(self, how, **kwargs): how : string / cython mapped function **kwargs : kw args passed to how function """ - # we may need to actually resample as if we are timestamps if self.kind == "timestamp": return super()._downsample(how, **kwargs) @@ -1204,7 +1197,6 @@ def _upsample(self, method, limit=None, fill_value=None): .fillna """ - # we may need to actually resample as if we are timestamps if self.kind == "timestamp": return super()._upsample(method, limit=limit, fill_value=fill_value) @@ -1279,7 +1271,6 @@ def get_resampler_for_grouping( """ Return our appropriate resampler when grouping as well. """ - # .resample uses 'on' similar to how .groupby uses 'key' kwargs["key"] = kwargs.pop("on", None) @@ -1787,8 +1778,8 @@ def asfreq(obj, freq, method=None, how=None, normalize=False, fill_value=None): elif len(obj.index) == 0: new_obj = obj.copy() - new_obj.index = obj.index._shallow_copy(freq=to_offset(freq)) + new_obj.index = _asfreq_compat(obj.index, freq) else: dti = date_range(obj.index[0], obj.index[-1], freq=freq) dti.name = obj.index.name @@ -1797,3 +1788,28 @@ def asfreq(obj, freq, method=None, how=None, normalize=False, fill_value=None): new_obj.index = new_obj.index.normalize() return new_obj + + +def _asfreq_compat(index, freq): + """ + Helper to mimic asfreq on (empty) DatetimeIndex and TimedeltaIndex. + + Parameters + ---------- + index : PeriodIndex, DatetimeIndex, or TimedeltaIndex + freq : DateOffset + + Returns + ------- + same type as index + """ + if len(index) != 0: + # This should never be reached, always checked by the caller + raise ValueError( + "Can only set arbitrary freq for empty DatetimeIndex or TimedeltaIndex" + ) + if isinstance(index, PeriodIndex): + new_index = index.asfreq(freq=freq) + else: + new_index = Index([], dtype=index.dtype, freq=freq, name=index.name) + return new_index diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py index 480c5279ad3f6..49ac1b6cfa52b 100644 --- a/pandas/core/reshape/merge.py +++ b/pandas/core/reshape/merge.py @@ -108,7 +108,6 @@ def _groupby_and_merge( check_duplicates: bool, default True should we check & clean duplicates """ - pieces = [] if not isinstance(by, (list, tuple)): by = [by] diff --git a/pandas/core/reshape/pivot.py b/pandas/core/reshape/pivot.py index 053fb86836ff8..b04e4e1ac4d48 100644 --- a/pandas/core/reshape/pivot.py +++ b/pandas/core/reshape/pivot.py @@ -425,17 +425,31 @@ def _convert_by(by): def pivot(data: "DataFrame", index=None, columns=None, values=None) -> "DataFrame": if columns is None: raise TypeError("pivot() missing 1 required argument: 'columns'") + columns = columns if is_list_like(columns) else [columns] if values is None: - cols = [columns] if index is None else [index, columns] + cols: List[str] = [] + if index is None: + pass + elif is_list_like(index): + cols = list(index) + else: + cols = [index] + cols.extend(columns) + append = index is None indexed = data.set_index(cols, append=append) else: if index is None: - index = data.index + index = [Series(data.index, name=data.index.name)] + elif is_list_like(index): + index = [data[idx] for idx in index] else: - index = data[index] - index = MultiIndex.from_arrays([index, data[columns]]) + index = [data[index]] + + data_columns = [data[col] for col in columns] + index.extend(data_columns) + index = MultiIndex.from_arrays(index) if is_list_like(values) and not isinstance(values, tuple): # Exclude tuple because it is seen as a single column name @@ -553,7 +567,6 @@ def crosstab( b 0 1 0 c 0 0 0 """ - index = com.maybe_make_list(index) columns = com.maybe_make_list(columns) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index a18b45a077be0..86417faf6cd11 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -513,7 +513,6 @@ def _format_labels( bins, precision: int, right: bool = True, include_lowest: bool = False, dtype=None ): """ based on the dtype, return our labels """ - closed = "right" if right else "left" if is_datetime64tz_dtype(dtype): @@ -544,7 +543,6 @@ def _preprocess_for_cut(x): input to array, strip the index information and store it separately """ - # Check that the passed array is a Pandas or Numpy object # We don't want to strip away a Pandas data-type here (e.g. datetimetz) ndim = getattr(x, "ndim", None) @@ -589,7 +587,8 @@ def _round_frac(x, precision: int): def _infer_precision(base_precision: int, bins) -> int: - """Infer an appropriate precision for _round_frac + """ + Infer an appropriate precision for _round_frac """ for precision in range(base_precision, 20): levels = [_round_frac(b, precision) for b in bins] diff --git a/pandas/core/series.py b/pandas/core/series.py index 0786674daf874..3ded02598963c 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -25,7 +25,7 @@ from pandas._libs import lib, properties, reshape, tslibs from pandas._typing import Label from pandas.compat.numpy import function as nv -from pandas.util._decorators import Appender, Substitution +from pandas.util._decorators import Appender, Substitution, doc from pandas.util._validators import validate_bool_kwarg, validate_percentile from pandas.core.dtypes.cast import convert_dtypes, validate_numeric_casting @@ -34,7 +34,6 @@ ensure_platform_int, is_bool, is_categorical_dtype, - is_datetime64_dtype, is_dict_like, is_extension_array_dtype, is_integer, @@ -42,7 +41,6 @@ is_list_like, is_object_dtype, is_scalar, - is_timedelta64_dtype, ) from pandas.core.dtypes.generic import ( ABCDataFrame, @@ -64,7 +62,7 @@ from pandas.core import algorithms, base, generic, nanops, ops from pandas.core.accessor import CachedAccessor from pandas.core.arrays import ExtensionArray, try_cast_to_ea -from pandas.core.arrays.categorical import Categorical, CategoricalAccessor +from pandas.core.arrays.categorical import CategoricalAccessor from pandas.core.arrays.sparse import SparseAccessor import pandas.core.common as com from pandas.core.construction import ( @@ -73,7 +71,8 @@ is_empty_data, sanitize_array, ) -from pandas.core.indexers import maybe_convert_indices +from pandas.core.generic import NDFrame +from pandas.core.indexers import maybe_convert_indices, unpack_1tuple from pandas.core.indexes.accessors import CombinedDatetimelikeProperties from pandas.core.indexes.api import ( Float64Index, @@ -325,7 +324,7 @@ def __init__( data = SingleBlockManager(data, index, fastpath=True) - generic.NDFrame.__init__(self, data, fastpath=True) + generic.NDFrame.__init__(self, data) self.name = name self._set_axis(0, index, fastpath=True) @@ -396,7 +395,6 @@ def _set_axis(self, axis, labels, fastpath: bool = False) -> None: """ Override generic, we want to set the _typ here. """ - if not fastpath: labels = ensure_index(labels) @@ -540,7 +538,6 @@ def _internal_get_values(self): numpy.ndarray Data of the Series. """ - return self._data.get_values() # ops @@ -815,7 +812,7 @@ def take(self, indices, axis=0, is_copy=None, **kwargs) -> "Series": new_values, index=new_index, fastpath=True ).__finalize__(self) - def _take_with_is_copy(self, indices, axis=0, **kwargs): + def _take_with_is_copy(self, indices, axis=0): """ Internal version of the `take` method that sets the `_is_copy` attribute to keep track of the parent dataframe (using in indexing @@ -824,7 +821,7 @@ def _take_with_is_copy(self, indices, axis=0, **kwargs): See the docstring of `take` for full explanation of the parameters. """ - return self.take(indices=indices, axis=axis, **kwargs) + return self.take(indices=indices, axis=axis) def _ixs(self, i: int, axis: int = 0): """ @@ -840,9 +837,9 @@ def _ixs(self, i: int, axis: int = 0): """ return self._values[i] - def _slice(self, slobj: slice, axis: int = 0, kind: str = "getitem") -> "Series": - assert kind in ["getitem", "iloc"] - slobj = self.index._convert_slice_indexer(slobj, kind=kind) + def _slice(self, slobj: slice, axis: int = 0) -> "Series": + # axis kwarg is retained for compat with NDFrame method + # _slice is *always* positional return self._get_values(slobj) def __getitem__(self, key): @@ -854,6 +851,8 @@ def __getitem__(self, key): key_is_scalar = is_scalar(key) if key_is_scalar: key = self.index._convert_scalar_indexer(key, kind="getitem") + elif isinstance(key, (list, tuple)): + key = unpack_1tuple(key) if key_is_scalar or isinstance(self.index, MultiIndex): # Otherwise index.get_value will raise InvalidIndexError @@ -862,7 +861,9 @@ def __getitem__(self, key): return result except InvalidIndexError: - pass + if not isinstance(self.index, MultiIndex): + raise + except (KeyError, ValueError): if isinstance(key, tuple) and isinstance(self.index, MultiIndex): # kludge @@ -884,25 +885,19 @@ def __getitem__(self, key): def _get_with(self, key): # other: fancy integer or otherwise if isinstance(key, slice): - return self._slice(key) + # _convert_slice_indexer to determing if this slice is positional + # or label based, and if the latter, convert to positional + slobj = self.index._convert_slice_indexer(key, kind="getitem") + return self._slice(slobj) elif isinstance(key, ABCDataFrame): raise TypeError( "Indexing a Series with DataFrame is not " "supported, use the appropriate DataFrame column" ) elif isinstance(key, tuple): - try: - return self._get_values_tuple(key) - except ValueError: - # if we don't have a MultiIndex, we may still be able to handle - # a 1-tuple. see test_1tuple_without_multiindex - if len(key) == 1: - key = key[0] - if isinstance(key, slice): - return self._get_values(key) - raise - - if not isinstance(key, (list, np.ndarray, Series, Index)): + return self._get_values_tuple(key) + + if not isinstance(key, (list, np.ndarray, ExtensionArray, Series, Index)): key = list(key) if isinstance(key, Index): @@ -913,23 +908,17 @@ def _get_with(self, key): # Note: The key_type == "boolean" case should be caught by the # com.is_bool_indexer check in __getitem__ if key_type == "integer": + # We need to decide whether to treat this as a positional indexer + # (i.e. self.iloc) or label-based (i.e. self.loc) if self.index.is_integer() or self.index.is_floating(): return self.loc[key] elif isinstance(self.index, IntervalIndex): - indexer = self.index.get_indexer_for(key) - return self.iloc[indexer] + return self.loc[key] else: - return self._get_values(key) + return self.iloc[key] - if isinstance(key, (list, tuple)): - # TODO: de-dup with tuple case handled above? + if isinstance(key, list): # handle the dup indexing case GH#4246 - if len(key) == 1 and isinstance(key[0], slice): - # [slice(0, 5, None)] will break if you convert to ndarray, - # e.g. as requested by np.median - # FIXME: hack - return self._get_values(key) - return self.loc[key] return self.reindex(key) @@ -979,7 +968,12 @@ def _get_value(self, label, takeable: bool = False): """ if takeable: return self._values[label] - return self.index.get_value(self, label) + + # Similar to Index.get_value, but we do not fall back to positional + loc = self.index.get_loc(label) + # We assume that _convert_scalar_indexer has already been called, + # with kind="loc", if necessary, by the time we get here + return self.index._get_values_for_loc(self, loc, label) def __setitem__(self, key, value): key = com.apply_if_callable(key, self) @@ -987,8 +981,6 @@ def __setitem__(self, key, value): try: self._set_with_engine(key, value) - except com.SettingWithCopyError: - raise except (KeyError, ValueError): values = self._values if is_integer(key) and not self.index.inferred_type == "integer": @@ -997,9 +989,6 @@ def __setitem__(self, key, value): self[:] = value else: self.loc[key] = value - except InvalidIndexError: - # e.g. slice - self._set_with(key, value) except TypeError as e: if isinstance(key, tuple) and not isinstance(self.index, MultiIndex): @@ -1070,7 +1059,7 @@ def _set_with(self, key, value): def _set_labels(self, key, value): key = com.asarray_tuplesafe(key) - indexer = self.index.get_indexer(key) + indexer: np.ndarray = self.index.get_indexer(key) mask = indexer == -1 if mask.any(): raise ValueError(f"{key[mask]} not contained in the index") @@ -1096,12 +1085,6 @@ def _set_value(self, label, value, takeable: bool = False): value : object Scalar value. takeable : interpret the index as indexers, default False - - Returns - ------- - Series - If label is contained, will be reference to calling Series, - otherwise a new object. """ try: if takeable: @@ -1115,8 +1098,6 @@ def _set_value(self, label, value, takeable: bool = False): # set using a non-recursive method self.loc[label] = value - return self - # ---------------------------------------------------------------------- # Unsorted @@ -1395,7 +1376,6 @@ def to_string( str or None String representation of Series if ``buf=None``, otherwise None. """ - formatter = fmt.SeriesFormatter( self, name=name, @@ -2164,7 +2144,6 @@ def quantile(self, q=0.5, interpolation="linear"): 0.75 3.25 dtype: float64 """ - validate_percentile(q) # We dispatch to DataFrame so that core.internals only has to worry @@ -2576,7 +2555,6 @@ def _binop(self, other, func, level=None, fill_value=None): ------- Series """ - if not isinstance(other, Series): raise AssertionError("Other operand must be Series") @@ -2974,11 +2952,11 @@ def sort_index( self, axis=0, level=None, - ascending=True, - inplace=False, - kind="quicksort", - na_position="last", - sort_remaining=True, + ascending: bool = True, + inplace: bool = False, + kind: str = "quicksort", + na_position: str = "last", + sort_remaining: bool = True, ignore_index: bool = False, ): """ @@ -2993,8 +2971,9 @@ def sort_index( Axis to direct sorting. This can only be 0 for Series. level : int, optional If not None, sort on values in specified index level(s). - ascending : bool, default true - Sort ascending vs. descending. + ascending : bool or list of bools, default True + Sort ascending vs. descending. When the index is a MultiIndex the + sort direction can be controlled for each level individually. inplace : bool, default False If True, perform operation in-place. kind : {'quicksort', 'mergesort', 'heapsort'}, default 'quicksort' @@ -3845,21 +3824,12 @@ def _reduce( if axis is not None: self._get_axis_number(axis) - if isinstance(delegate, Categorical): - return delegate._reduce(name, skipna=skipna, **kwds) - elif isinstance(delegate, ExtensionArray): + if isinstance(delegate, ExtensionArray): # dispatch to ExtensionArray interface return delegate._reduce(name, skipna=skipna, **kwds) - elif is_datetime64_dtype(delegate): - # use DatetimeIndex implementation to handle skipna correctly - delegate = DatetimeIndex(delegate) - elif is_timedelta64_dtype(delegate) and hasattr(TimedeltaIndex, name): - # use TimedeltaIndex to handle skipna correctly - # TODO: remove hasattr check after TimedeltaIndex has `std` method - delegate = TimedeltaIndex(delegate) - - # dispatch to numpy arrays - elif isinstance(delegate, np.ndarray): + + else: + # dispatch to numpy arrays if numeric_only: raise NotImplementedError( f"Series.{name} does not implement numeric_only." @@ -3867,19 +3837,6 @@ def _reduce( with np.errstate(all="ignore"): return op(delegate, skipna=skipna, **kwds) - # TODO(EA) dispatch to Index - # remove once all internals extension types are - # moved to ExtensionArrays - return delegate._reduce( - op=op, - name=name, - axis=axis, - skipna=skipna, - numeric_only=numeric_only, - filter_type=filter_type, - **kwds, - ) - def _reindex_indexer(self, new_index, indexer, copy): if indexer is None: if copy: @@ -4003,6 +3960,8 @@ def rename( @Appender( """ + Examples + -------- >>> s = pd.Series([1, 2, 3]) >>> s 0 1 @@ -4142,8 +4101,7 @@ def drop( errors=errors, ) - @Substitution(**_shared_doc_kwargs) - @Appender(generic.NDFrame.fillna.__doc__) + @doc(NDFrame.fillna, **_shared_doc_kwargs) def fillna( self, value=None, @@ -4376,7 +4334,7 @@ def between(self, left, right, inclusive=True) -> "Series": # Convert to types that support pd.NA def _convert_dtypes( - self: ABCSeries, + self, infer_objects: bool = True, convert_string: bool = True, convert_integer: bool = True, @@ -4560,6 +4518,14 @@ def to_period(self, freq=None, copy=True) -> "Series": # ---------------------------------------------------------------------- # Add index + _AXIS_ORDERS = ["index"] + _AXIS_NUMBERS = {"index": 0} + _AXIS_NAMES = {0: "index"} + _AXIS_REVERSED = False + _AXIS_LEN = len(_AXIS_ORDERS) + _info_axis_number = 0 + _info_axis_name = "index" + index: "Index" = properties.AxisProperty( axis=0, doc="The index (axis labels) of the Series." ) @@ -4578,7 +4544,6 @@ def to_period(self, freq=None, copy=True) -> "Series": hist = pandas.plotting.hist_series -Series._setup_axes(["index"], docs={"index": "The index (axis labels) of the Series."}) Series._add_numeric_operations() Series._add_series_or_dataframe_operations() diff --git a/pandas/core/sorting.py b/pandas/core/sorting.py index 51c154aa47518..5496eca46b992 100644 --- a/pandas/core/sorting.py +++ b/pandas/core/sorting.py @@ -376,7 +376,6 @@ def compress_group_index(group_index, sort: bool = True): space can be huge, so this function compresses it, by computing offsets (comp_ids) into the list of unique labels (obs_group_ids). """ - size_hint = min(len(group_index), hashtable._SIZE_HINT_LIMIT) table = hashtable.Int64HashTable(size_hint) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index 18c7504f2c2f8..4b0fc3e47356c 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -349,7 +349,6 @@ def str_contains(arr, pat, case=True, flags=0, na=np.nan, regex=True): Examples -------- - Returning a Series of booleans using only a literal pattern. >>> s1 = pd.Series(['Mouse', 'dog', 'house and parrot', '23', np.NaN]) @@ -687,7 +686,6 @@ def str_replace(arr, pat, repl, n=-1, case=None, flags=0, regex=True): 2 NaN dtype: object """ - # Check whether repl is valid (GH 13438, GH 15055) if not (isinstance(repl, str) or callable(repl)): raise TypeError("repl must be a string or callable") @@ -1085,7 +1083,6 @@ def str_extractall(arr, pat, flags=0): B 0 b 1 C 0 NaN 1 """ - regex = re.compile(pat, flags=flags) # the regex must contain capture groups. if regex.groups == 0: @@ -1276,7 +1273,6 @@ def str_findall(arr, pat, flags=0): Examples -------- - >>> s = pd.Series(['Lion', 'Monkey', 'Rabbit']) The search for the pattern 'Monkey' returns one match: @@ -1358,7 +1354,6 @@ def str_find(arr, sub, start=0, end=None, side="left"): Series or Index Indexes where substring is found. """ - if not isinstance(sub, str): msg = f"expected a string object, not {type(sub).__name__}" raise TypeError(msg) @@ -1746,7 +1741,6 @@ def str_wrap(arr, width, **kwargs): Examples -------- - >>> s = pd.Series(['line to be wrapped', 'another line to be wrapped']) >>> s.str.wrap(12) 0 line to be\nwrapped @@ -1930,7 +1924,6 @@ def forbid_nonstring_types(forbidden, name=None): TypeError If the inferred type of the underlying data is in `forbidden`. """ - # deal with None forbidden = [] if forbidden is None else forbidden @@ -2013,7 +2006,7 @@ def wrapper3(self, pat, na=np.nan): def copy(source): - "Copy a docstring from another source function (if present)" + """Copy a docstring from another source function (if present)""" def do_copy(target): if source.__doc__: diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index 6d45ddd29d783..b10b736b9134e 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -2,7 +2,7 @@ from datetime import datetime, time from functools import partial from itertools import islice -from typing import Optional, TypeVar, Union +from typing import List, Optional, TypeVar, Union import numpy as np @@ -296,7 +296,9 @@ def _convert_listlike_datetimes( if not isinstance(arg, (DatetimeArray, DatetimeIndex)): return DatetimeIndex(arg, tz=tz, name=name) if tz == "utc": - arg = arg.tz_convert(None).tz_localize(tz) + # error: Item "DatetimeIndex" of "Union[DatetimeArray, DatetimeIndex]" has + # no attribute "tz_convert" + arg = arg.tz_convert(None).tz_localize(tz) # type: ignore return arg elif is_datetime64_ns_dtype(arg): @@ -307,7 +309,9 @@ def _convert_listlike_datetimes( pass elif tz: # DatetimeArray, DatetimeIndex - return arg.tz_localize(tz) + # error: Item "DatetimeIndex" of "Union[DatetimeArray, DatetimeIndex]" has + # no attribute "tz_localize" + return arg.tz_localize(tz) # type: ignore return arg @@ -826,18 +830,18 @@ def f(value): required = ["year", "month", "day"] req = sorted(set(required) - set(unit_rev.keys())) if len(req): - required = ",".join(req) + _required = ",".join(req) raise ValueError( "to assemble mappings requires at least that " - f"[year, month, day] be specified: [{required}] is missing" + f"[year, month, day] be specified: [{_required}] is missing" ) # keys we don't recognize excess = sorted(set(unit_rev.keys()) - set(_unit_map.values())) if len(excess): - excess = ",".join(excess) + _excess = ",".join(excess) raise ValueError( - f"extra keys have been passed to the datetime assemblage: [{excess}]" + f"extra keys have been passed to the datetime assemblage: [{_excess}]" ) def coerce(values): @@ -992,7 +996,7 @@ def _convert_listlike(arg, format): if infer_time_format and format is None: format = _guess_time_format_for_array(arg) - times = [] + times: List[Optional[time]] = [] if format is not None: for element in arg: try: diff --git a/pandas/core/tools/timedeltas.py b/pandas/core/tools/timedeltas.py index 3f0cfce39f6f9..d7529ec799022 100644 --- a/pandas/core/tools/timedeltas.py +++ b/pandas/core/tools/timedeltas.py @@ -53,7 +53,6 @@ def to_timedelta(arg, unit="ns", errors="raise"): Examples -------- - Parsing a single string to a Timedelta: >>> pd.to_timedelta('1 days 06:05:01.00003') @@ -111,7 +110,6 @@ def to_timedelta(arg, unit="ns", errors="raise"): def _coerce_scalar_to_timedelta_type(r, unit="ns", errors="raise"): """Convert string 'r' to a timedelta object.""" - try: result = Timedelta(r, unit) except ValueError: @@ -128,7 +126,6 @@ def _coerce_scalar_to_timedelta_type(r, unit="ns", errors="raise"): def _convert_listlike(arg, unit="ns", errors="raise", name=None): """Convert a list of objects to a timedelta index object.""" - if isinstance(arg, (list, tuple)) or not hasattr(arg, "dtype"): # This is needed only to ensure that in the case where we end up # returning arg (errors == "ignore"), and where the input is a diff --git a/pandas/core/util/hashing.py b/pandas/core/util/hashing.py index 3366f10b92604..d9c8611c94cdb 100644 --- a/pandas/core/util/hashing.py +++ b/pandas/core/util/hashing.py @@ -269,7 +269,6 @@ def hash_array( ------- 1d uint64 numpy array of hash values, same length as the vals """ - if not hasattr(vals, "dtype"): raise TypeError("must pass a ndarray-like") dtype = vals.dtype @@ -295,7 +294,7 @@ def hash_array( elif issubclass(dtype.type, (np.datetime64, np.timedelta64)): vals = vals.view("i8").astype("u8", copy=False) elif issubclass(dtype.type, np.number) and dtype.itemsize <= 8: - vals = vals.view("u{}".format(vals.dtype.itemsize)).astype("u8") + vals = vals.view(f"u{vals.dtype.itemsize}").astype("u8") else: # With repeated values, its MUCH faster to categorize object dtypes, # then hash and rename categories. We allow skipping the categorization @@ -340,7 +339,6 @@ def _hash_scalar( ------- 1d uint64 numpy array of hash value, of length 1 """ - if isna(val): # this is to be consistent with the _hash_categorical implementation return np.array([np.iinfo(np.uint64).max], dtype="u8") diff --git a/pandas/core/window/ewm.py b/pandas/core/window/ewm.py index 37e3cd42f2115..e045d1c2211d7 100644 --- a/pandas/core/window/ewm.py +++ b/pandas/core/window/ewm.py @@ -98,7 +98,6 @@ class EWM(_Rolling): Examples -------- - >>> df = pd.DataFrame({'B': [0, 1, 2, np.nan, 4]}) >>> df B @@ -116,6 +115,7 @@ class EWM(_Rolling): 3 1.615385 4 3.670213 """ + _attributes = ["com", "min_periods", "adjust", "ignore_na", "axis"] def __init__( diff --git a/pandas/core/window/expanding.py b/pandas/core/window/expanding.py index a0bf3376d2352..140e0144d0a2d 100644 --- a/pandas/core/window/expanding.py +++ b/pandas/core/window/expanding.py @@ -37,7 +37,6 @@ class Expanding(_Rolling_and_Expanding): Examples -------- - >>> df = pd.DataFrame({'B': [0, 1, 2, np.nan, 4]}) B 0 0.0 diff --git a/pandas/core/window/numba_.py b/pandas/core/window/numba_.py index 127957943d2ff..d6e8194c861fa 100644 --- a/pandas/core/window/numba_.py +++ b/pandas/core/window/numba_.py @@ -110,7 +110,6 @@ def generate_numba_apply_func( ------- Numba function """ - if engine_kwargs is None: engine_kwargs = {} diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 580c7cc0554d0..65ac064a1322e 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -149,7 +149,6 @@ def _create_blocks(self): """ Split data into blocks & return conformed data. """ - obj = self._selected_obj # filter out the on from the object @@ -172,7 +171,6 @@ def _gotitem(self, key, ndim, subset=None): subset : object, default None subset to act on """ - # create a new object to prevent aliasing if subset is None: subset = self.obj @@ -238,7 +236,6 @@ def __repr__(self) -> str: """ Provide a nice str repr of our rolling object. """ - attrs_list = ( f"{attr_name}={getattr(self, attr_name)}" for attr_name in self._attributes @@ -284,7 +281,6 @@ def _wrap_result(self, result, block=None, obj=None): """ Wrap a single result. """ - if obj is None: obj = self._selected_obj index = obj.index @@ -310,7 +306,6 @@ def _wrap_results(self, results, blocks, obj, exclude=None) -> FrameOrSeries: obj : conformed data (may be resampled) exclude: list of columns to exclude, default to None """ - from pandas import Series, concat final = [] @@ -851,7 +846,6 @@ class Window(_Window): Examples -------- - >>> df = pd.DataFrame({'B': [0, 1, 2, np.nan, 4]}) >>> df B @@ -1021,7 +1015,6 @@ def _get_window( window : ndarray the window, weights """ - window = self.window if isinstance(window, (list, tuple, np.ndarray)): return com.asarray_tuplesafe(window).astype(float) @@ -1296,13 +1289,14 @@ def apply( raise ValueError("engine must be either 'numba' or 'cython'") # TODO: Why do we always pass center=False? - # name=func for WindowGroupByMixin._apply + # name=func & raw=raw for WindowGroupByMixin._apply return self._apply( apply_func, center=False, floor=0, name=func, use_numba_cache=engine == "numba", + raw=raw, ) def _generate_cython_apply_func(self, args, kwargs, raw, offset, func): @@ -1781,7 +1775,7 @@ def corr(self, other=None, pairwise=None, **kwargs): # only default unset pairwise = True if pairwise is None else pairwise other = self._shallow_copy(other) - window = self._get_window(other) + window = self._get_window(other) if not self.is_freq_type else self.win_freq def _get_corr(a, b): a = a.rolling( diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index ebe9a3d5bf472..29e69cc5fe509 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -4,6 +4,8 @@ Expose public exceptions & warnings """ +from pandas._config.config import OptionError + from pandas._libs.tslibs import NullFrequencyError, OutOfBoundsDatetime diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index f808b7e706afb..f4bd14ad5c679 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -126,7 +126,7 @@ def copy_osx_pyobjc(text): board.setData_forType_(newData, AppKit.NSStringPboardType) def paste_osx_pyobjc(): - "Returns contents of clipboard" + """Returns contents of clipboard""" board = AppKit.NSPasteboard.generalPasteboard() content = board.stringForType_(AppKit.NSStringPboardType) return content @@ -500,7 +500,6 @@ def determine_clipboard(): Determine the OS/platform and set the copy() and paste() functions accordingly. """ - global Foundation, AppKit, qtpy, PyQt4, PyQt5 # Setup for the CYGWIN platform: diff --git a/pandas/io/common.py b/pandas/io/common.py index c4772895afd1e..c52583eed27ec 100644 --- a/pandas/io/common.py +++ b/pandas/io/common.py @@ -74,8 +74,9 @@ def is_url(url) -> bool: def _expand_user( filepath_or_buffer: FilePathOrBuffer[AnyStr], ) -> FilePathOrBuffer[AnyStr]: - """Return the argument with an initial component of ~ or ~user - replaced by that user's home directory. + """ + Return the argument with an initial component of ~ or ~user + replaced by that user's home directory. Parameters ---------- @@ -103,7 +104,8 @@ def validate_header_arg(header) -> None: def stringify_path( filepath_or_buffer: FilePathOrBuffer[AnyStr], ) -> FilePathOrBuffer[AnyStr]: - """Attempt to convert a path-like object to a string. + """ + Attempt to convert a path-like object to a string. Parameters ---------- @@ -296,7 +298,6 @@ def infer_compression( ------ ValueError on invalid compression specified. """ - # No compression has been explicitly specified if compression is None: return None diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py index 5ad56e30eeb39..97959bd125113 100644 --- a/pandas/io/excel/_base.py +++ b/pandas/io/excel/_base.py @@ -721,7 +721,8 @@ def _get_sheet_name(self, sheet_name): return sheet_name def _value_with_fmt(self, val): - """Convert numpy types to Python types for the Excel writers. + """ + Convert numpy types to Python types for the Excel writers. Parameters ---------- @@ -755,8 +756,10 @@ def _value_with_fmt(self, val): @classmethod def check_extension(cls, ext): - """checks that path's extension against the Writer's supported - extensions. If it isn't supported, raises UnsupportedFiletypeError.""" + """ + checks that path's extension against the Writer's supported + extensions. If it isn't supported, raises UnsupportedFiletypeError. + """ if ext.startswith("."): ext = ext[1:] if not any(ext in extension for extension in cls.supported_extensions): diff --git a/pandas/io/excel/_odfreader.py b/pandas/io/excel/_odfreader.py index ec5f6fcb17ff8..7af776dc1a10f 100644 --- a/pandas/io/excel/_odfreader.py +++ b/pandas/io/excel/_odfreader.py @@ -64,7 +64,8 @@ def get_sheet_by_name(self, name: str): raise ValueError(f"sheet {name} not found") def get_sheet_data(self, sheet, convert_float: bool) -> List[List[Scalar]]: - """Parse an ODF Table into a list of lists + """ + Parse an ODF Table into a list of lists """ from odf.table import CoveredTableCell, TableCell, TableRow @@ -120,7 +121,8 @@ def get_sheet_data(self, sheet, convert_float: bool) -> List[List[Scalar]]: return table def _get_row_repeat(self, row) -> int: - """Return number of times this row was repeated + """ + Return number of times this row was repeated Repeating an empty row appeared to be a common way of representing sparse rows in the table. """ @@ -134,7 +136,8 @@ def _get_column_repeat(self, cell) -> int: return int(cell.attributes.get((TABLENS, "number-columns-repeated"), 1)) def _is_empty_row(self, row) -> bool: - """Helper function to find empty rows + """ + Helper function to find empty rows """ for column in row.childNodes: if len(column.childNodes) > 0: diff --git a/pandas/io/excel/_openpyxl.py b/pandas/io/excel/_openpyxl.py index be52523e486af..a96c0f814e2d8 100644 --- a/pandas/io/excel/_openpyxl.py +++ b/pandas/io/excel/_openpyxl.py @@ -51,7 +51,6 @@ def _convert_to_style(cls, style_dict): ---------- style_dict : style dictionary to convert """ - from openpyxl.style import Style xls_style = Style() @@ -92,7 +91,6 @@ def _convert_to_style_kwargs(cls, style_dict): value has been replaced with a native openpyxl style object of the appropriate class. """ - _style_key_map = {"borders": "border"} style_kwargs = {} @@ -128,7 +126,6 @@ def _convert_to_color(cls, color_spec): ------- color : openpyxl.styles.Color """ - from openpyxl.styles import Color if isinstance(color_spec, str): @@ -164,7 +161,6 @@ def _convert_to_font(cls, font_dict): ------- font : openpyxl.styles.Font """ - from openpyxl.styles import Font _font_key_map = { @@ -202,7 +198,6 @@ def _convert_to_stop(cls, stop_seq): ------- stop : list of openpyxl.styles.Color """ - return map(cls._convert_to_color, stop_seq) @classmethod @@ -230,7 +225,6 @@ def _convert_to_fill(cls, fill_dict): ------- fill : openpyxl.styles.Fill """ - from openpyxl.styles import PatternFill, GradientFill _pattern_fill_key_map = { @@ -286,7 +280,6 @@ def _convert_to_side(cls, side_spec): ------- side : openpyxl.styles.Side """ - from openpyxl.styles import Side _side_key_map = {"border_style": "style"} @@ -329,7 +322,6 @@ def _convert_to_border(cls, border_dict): ------- border : openpyxl.styles.Border """ - from openpyxl.styles import Border _border_key_map = {"diagonalup": "diagonalUp", "diagonaldown": "diagonalDown"} @@ -365,7 +357,6 @@ def _convert_to_alignment(cls, alignment_dict): ------- alignment : openpyxl.styles.Alignment """ - from openpyxl.styles import Alignment return Alignment(**alignment_dict) @@ -375,11 +366,13 @@ def _convert_to_number_format(cls, number_format_dict): """ Convert ``number_format_dict`` to an openpyxl v2.1.0 number format initializer. + Parameters ---------- number_format_dict : dict A dict with zero or more of the following keys. 'format_code' : str + Returns ------- number_format : str @@ -390,16 +383,17 @@ def _convert_to_number_format(cls, number_format_dict): def _convert_to_protection(cls, protection_dict): """ Convert ``protection_dict`` to an openpyxl v2 Protection object. + Parameters ---------- protection_dict : dict A dict with zero or more of the following keys. 'locked' 'hidden' + Returns ------- """ - from openpyxl.styles import Protection return Protection(**protection_dict) @@ -474,7 +468,8 @@ def write_cells( class _OpenpyxlReader(_BaseExcelReader): def __init__(self, filepath_or_buffer: FilePathOrBuffer) -> None: - """Reader using openpyxl engine. + """ + Reader using openpyxl engine. Parameters ---------- diff --git a/pandas/io/excel/_util.py b/pandas/io/excel/_util.py index 9d284c8031840..c8d40d7141fc8 100644 --- a/pandas/io/excel/_util.py +++ b/pandas/io/excel/_util.py @@ -171,9 +171,11 @@ def _trim_excel_header(row): def _fill_mi_header(row, control_row): - """Forward fill blank entries in row but only inside the same parent index. + """ + Forward fill blank entries in row but only inside the same parent index. Used for creating headers in Multiindex. + Parameters ---------- row : list diff --git a/pandas/io/excel/_xlrd.py b/pandas/io/excel/_xlrd.py index be1b78eeb146e..8f7d3b1368fc7 100644 --- a/pandas/io/excel/_xlrd.py +++ b/pandas/io/excel/_xlrd.py @@ -9,7 +9,8 @@ class _XlrdReader(_BaseExcelReader): def __init__(self, filepath_or_buffer): - """Reader using xlrd engine. + """ + Reader using xlrd engine. Parameters ---------- @@ -57,9 +58,9 @@ def get_sheet_data(self, sheet, convert_float): epoch1904 = self.book.datemode def _parse_cell(cell_contents, cell_typ): - """converts the contents of the cell into a pandas - appropriate object""" - + """ + converts the contents of the cell into a pandas appropriate object + """ if cell_typ == XL_CELL_DATE: # Use the newer xlrd datetime handling. diff --git a/pandas/io/excel/_xlsxwriter.py b/pandas/io/excel/_xlsxwriter.py index 6d9ff9be5249a..85a1bb031f457 100644 --- a/pandas/io/excel/_xlsxwriter.py +++ b/pandas/io/excel/_xlsxwriter.py @@ -85,7 +85,6 @@ def convert(cls, style_dict, num_format_str=None): style_dict : style dictionary to convert num_format_str : optional number format string """ - # Create a XlsxWriter format object. props = {} @@ -191,7 +190,6 @@ def save(self): """ Save workbook to disk. """ - return self.book.close() def write_cells( diff --git a/pandas/io/excel/_xlwt.py b/pandas/io/excel/_xlwt.py index d102a885cef0a..78efe77e9fe2d 100644 --- a/pandas/io/excel/_xlwt.py +++ b/pandas/io/excel/_xlwt.py @@ -80,7 +80,8 @@ def write_cells( def _style_to_xlwt( cls, item, firstlevel: bool = True, field_sep=",", line_sep=";" ) -> str: - """helper which recursively generate an xlwt easy style string + """ + helper which recursively generate an xlwt easy style string for example: hstyle = {"font": {"bold": True}, diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 28a069bc9fc1b..aac1df5dcd396 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -41,7 +41,8 @@ def __init__( class CSSToExcelConverter: - """A callable for converting CSS declarations to ExcelWriter styles + """ + A callable for converting CSS declarations to ExcelWriter styles Supports parts of CSS 2.2, with minimal CSS 3.0 support (e.g. text-shadow), focusing on font styling, backgrounds, borders and alignment. diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index 149533bf0c238..b5ddd15c1312a 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -187,7 +187,7 @@ def _get_footer(self) -> str: if self.length: if footer: footer += ", " - footer += "Length: {length}".format(length=len(self.categorical)) + footer += f"Length: {len(self.categorical)}" level_info = self.categorical._repr_categories_info() @@ -217,7 +217,6 @@ def to_string(self) -> str: fmt_values = self._get_formatted_values() - fmt_values = ["{i}".format(i=i) for i in fmt_values] fmt_values = [i.strip() for i in fmt_values] values = ", ".join(fmt_values) result = ["[" + values + "]"] @@ -301,28 +300,26 @@ def _get_footer(self) -> str: assert isinstance( self.series.index, (ABCDatetimeIndex, ABCPeriodIndex, ABCTimedeltaIndex) ) - footer += "Freq: {freq}".format(freq=self.series.index.freqstr) + footer += f"Freq: {self.series.index.freqstr}" if self.name is not False and name is not None: if footer: footer += ", " series_name = pprint_thing(name, escape_chars=("\t", "\r", "\n")) - footer += ( - ("Name: {sname}".format(sname=series_name)) if name is not None else "" - ) + footer += f"Name: {series_name}" if self.length is True or (self.length == "truncate" and self.truncate_v): if footer: footer += ", " - footer += "Length: {length}".format(length=len(self.series)) + footer += f"Length: {len(self.series)}" if self.dtype is not False and self.dtype is not None: - name = getattr(self.tr_series.dtype, "name", None) - if name: + dtype_name = getattr(self.tr_series.dtype, "name", None) + if dtype_name: if footer: footer += ", " - footer += "dtype: {typ}".format(typ=pprint_thing(name)) + footer += f"dtype: {pprint_thing(dtype_name)}" # level infos are added to the end and in a new line, like it is done # for Categoricals @@ -359,9 +356,7 @@ def to_string(self) -> str: footer = self._get_footer() if len(series) == 0: - return "{name}([], {footer})".format( - name=type(self.series).__name__, footer=footer - ) + return f"{type(self.series).__name__}([], {footer})" fmt_index, have_header = self._get_formatted_index() fmt_values = self._get_formatted_values() @@ -584,10 +579,8 @@ def __init__( self.formatters = formatters else: raise ValueError( - ( - "Formatters length({flen}) should match " - "DataFrame number of columns({dlen})" - ).format(flen=len(formatters), dlen=len(frame.columns)) + f"Formatters length({len(formatters)}) should match " + f"DataFrame number of columns({len(frame.columns)})" ) self.na_rep = na_rep self.decimal = decimal @@ -816,10 +809,10 @@ def write_result(self, buf: IO[str]) -> None: frame = self.frame if len(frame.columns) == 0 or len(frame.index) == 0: - info_line = "Empty {name}\nColumns: {col}\nIndex: {idx}".format( - name=type(self.frame).__name__, - col=pprint_thing(frame.columns), - idx=pprint_thing(frame.index), + info_line = ( + f"Empty {type(self.frame).__name__}\n" + f"Columns: {pprint_thing(frame.columns)}\n" + f"Index: {pprint_thing(frame.index)}" ) text = info_line else: @@ -865,11 +858,7 @@ def write_result(self, buf: IO[str]) -> None: buf.writelines(text) if self.should_show_dimensions: - buf.write( - "\n\n[{nrows} rows x {ncols} columns]".format( - nrows=len(frame), ncols=len(frame.columns) - ) - ) + buf.write(f"\n\n[{len(frame)} rows x {len(frame.columns)} columns]") def _join_multiline(self, *args) -> str: lwidth = self.line_width @@ -932,7 +921,6 @@ def to_latex( """ Render a DataFrame to a LaTeX tabular/longtable environment output. """ - from pandas.io.formats.latex import LatexFormatter return LatexFormatter( @@ -979,7 +967,7 @@ def to_html( border : int A ``border=border`` attribute is included in the opening ```` tag. Default ``pd.options.display.html.border``. - """ + """ from pandas.io.formats.html import HTMLFormatter, NotebookFormatter Klass = NotebookFormatter if notebook else HTMLFormatter @@ -1075,7 +1063,7 @@ def _get_formatted_index(self, frame: "DataFrame") -> List[str]: # empty space for columns if self.show_col_idx_names: - col_header = ["{x}".format(x=x) for x in self._get_column_name_list()] + col_header = [str(x) for x in self._get_column_name_list()] else: col_header = [""] * columns.nlevels @@ -1135,7 +1123,6 @@ def format_array( ------- List[str] """ - fmt_klass: Type[GenericArrayFormatter] if is_datetime64_dtype(values.dtype): fmt_klass = Datetime64Formatter @@ -1211,10 +1198,8 @@ def _format_strings(self) -> List[str]: if self.float_format is None: float_format = get_option("display.float_format") if float_format is None: - fmt_str = "{{x: .{prec:d}g}}".format( - prec=get_option("display.precision") - ) - float_format = lambda x: fmt_str.format(x=x) + precision = get_option("display.precision") + float_format = lambda x: f"{x: .{precision:d}g}" else: float_format = self.float_format @@ -1240,10 +1225,10 @@ def _format(x): pass return self.na_rep elif isinstance(x, PandasObject): - return "{x}".format(x=x) + return str(x) else: # object dtype - return "{x}".format(x=formatter(x)) + return str(formatter(x)) vals = self.values if isinstance(vals, Index): @@ -1259,7 +1244,7 @@ def _format(x): fmt_values = [] for i, v in enumerate(vals): if not is_float_type[i] and leading_space: - fmt_values.append(" {v}".format(v=_format(v))) + fmt_values.append(f" {_format(v)}") elif is_float_type[i]: fmt_values.append(float_format(v)) else: @@ -1296,9 +1281,7 @@ def _value_formatter( float_format: Optional[float_format_type] = None, threshold: Optional[Union[float, int]] = None, ) -> Callable: - """Returns a function to be applied on each value to format it - """ - + """Returns a function to be applied on each value to format it""" # the float_format parameter supersedes self.float_format if float_format is None: float_format = self.float_format @@ -1346,7 +1329,6 @@ def get_result_as_array(self) -> np.ndarray: Returns the float values converted into strings using the parameters given at initialisation, as a numpy array """ - if self.formatter is not None: return np.array([self.formatter(x) for x in self.values]) @@ -1442,7 +1424,7 @@ def _format_strings(self) -> List[str]: class IntArrayFormatter(GenericArrayFormatter): def _format_strings(self) -> List[str]: - formatter = self.formatter or (lambda x: "{x: d}".format(x=x)) + formatter = self.formatter or (lambda x: f"{x: d}") fmt_values = [formatter(x) for x in self.values] return fmt_values @@ -1461,7 +1443,6 @@ def __init__( def _format_strings(self) -> List[str]: """ we by definition have DO NOT have a TZ """ - values = self.values if not isinstance(values, DatetimeIndex): @@ -1541,7 +1522,6 @@ def format_percentiles( >>> format_percentiles([0, 0.5, 0.02001, 0.5, 0.666666, 0.9999]) ['0%', '50%', '2.0%', '50%', '66.67%', '99.99%'] """ - percentiles = np.asarray(percentiles) # It checks for np.NaN as well @@ -1642,7 +1622,6 @@ def _get_format_datetime64_from_values( values: Union[np.ndarray, DatetimeArray, DatetimeIndex], date_format: Optional[str] ) -> Optional[str]: """ given values and a date_format, return a string format """ - if isinstance(values, np.ndarray) and values.ndim > 1: # We don't actually care about the order of values, and DatetimeIndex # only accepts 1D values @@ -1657,7 +1636,6 @@ def _get_format_datetime64_from_values( class Datetime64TZFormatter(Datetime64Formatter): def _format_strings(self) -> List[str]: """ we by definition have a TZ """ - values = self.values.astype(object) is_dates_only = _is_dates_only(values) formatter = self.formatter or _get_format_datetime64( @@ -1698,7 +1676,6 @@ def _get_format_timedelta64( If box, then show the return in quotes """ - values_int = values.astype(np.int64) consider_values = values_int != iNaT @@ -1726,7 +1703,7 @@ def _formatter(x): x = Timedelta(x) result = x._repr_base(format=format) if box: - result = "'{res}'".format(res=result) + result = f"'{result}'" return result return _formatter @@ -1842,7 +1819,8 @@ def __init__(self, accuracy: Optional[int] = None, use_eng_prefix: bool = False) self.use_eng_prefix = use_eng_prefix def __call__(self, num: Union[int, float]) -> str: - """ Formats a number in engineering notation, appending a letter + """ + Formats a number in engineering notation, appending a letter representing the power of 1000 of the original number. Some examples: >>> format_eng(0) # for self.accuracy = 0 @@ -1889,16 +1867,16 @@ def __call__(self, num: Union[int, float]) -> str: prefix = self.ENG_PREFIXES[int_pow10] else: if int_pow10 < 0: - prefix = "E-{pow10:02d}".format(pow10=-int_pow10) + prefix = f"E-{-int_pow10:02d}" else: - prefix = "E+{pow10:02d}".format(pow10=int_pow10) + prefix = f"E+{int_pow10:02d}" mant = sign * dnum / (10 ** pow10) if self.accuracy is None: # pragma: no cover format_str = "{mant: g}{prefix}" else: - format_str = "{{mant: .{acc:d}f}}{{prefix}}".format(acc=self.accuracy) + format_str = f"{{mant: .{self.accuracy:d}f}}{{prefix}}" formatted = format_str.format(mant=mant, prefix=prefix) @@ -1913,7 +1891,6 @@ def set_eng_float_format(accuracy: int = 3, use_eng_prefix: bool = False) -> Non See also EngFormatter. """ - set_option("display.float_format", EngFormatter(accuracy, use_eng_prefix)) set_option("display.column_space", max(12, accuracy + 9)) @@ -1941,7 +1918,8 @@ def _binify(cols: List[int], line_width: int) -> List[int]: def get_level_lengths( levels: Any, sentinel: Union[bool, object, str] = "" ) -> List[Dict[int, int]]: - """For each index in each level the function returns lengths of indexes. + """ + For each index in each level the function returns lengths of indexes. Parameters ---------- diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py index e3161415fe2bc..585e1af3dbc01 100644 --- a/pandas/io/formats/html.py +++ b/pandas/io/formats/html.py @@ -56,7 +56,7 @@ def __init__( self.table_id = self.fmt.table_id self.render_links = self.fmt.render_links if isinstance(self.fmt.col_space, int): - self.fmt.col_space = "{colspace}px".format(colspace=self.fmt.col_space) + self.fmt.col_space = f"{self.fmt.col_space}px" @property def show_row_idx_names(self) -> bool: @@ -124,7 +124,7 @@ def write_th( """ if header and self.fmt.col_space is not None: tags = tags or "" - tags += 'style="min-width: {colspace};"'.format(colspace=self.fmt.col_space) + tags += f'style="min-width: {self.fmt.col_space};"' self._write_cell(s, kind="th", indent=indent, tags=tags) @@ -135,9 +135,9 @@ def _write_cell( self, s: Any, kind: str = "td", indent: int = 0, tags: Optional[str] = None ) -> None: if tags is not None: - start_tag = "<{kind} {tags}>".format(kind=kind, tags=tags) + start_tag = f"<{kind} {tags}>" else: - start_tag = "<{kind}>".format(kind=kind) + start_tag = f"<{kind}>" if self.escape: # escape & first to prevent double escaping of & @@ -149,17 +149,12 @@ def _write_cell( if self.render_links and is_url(rs): rs_unescaped = pprint_thing(s, escape_chars={}).strip() - start_tag += ''.format(url=rs_unescaped) + start_tag += f'' end_a = "" else: end_a = "" - self.write( - "{start}{rs}{end_a}".format( - start=start_tag, rs=rs, end_a=end_a, kind=kind - ), - indent, - ) + self.write(f"{start_tag}{rs}{end_a}", indent) def write_tr( self, @@ -177,7 +172,7 @@ def write_tr( if align is None: self.write("", indent) else: - self.write(''.format(align=align), indent) + self.write(f'', indent) indent += indent_delta for i, s in enumerate(line): @@ -196,9 +191,7 @@ def render(self) -> List[str]: if self.should_show_dimensions: by = chr(215) # × self.write( - "

{rows} rows {by} {cols} columns

".format( - rows=len(self.frame), by=by, cols=len(self.frame.columns) - ) + f"

{len(self.frame)} rows {by} {len(self.frame.columns)} columns

" ) return self.elements @@ -224,12 +217,10 @@ def _write_table(self, indent: int = 0) -> None: if self.table_id is None: id_section = "" else: - id_section = ' id="{table_id}"'.format(table_id=self.table_id) + id_section = f' id="{self.table_id}"' self.write( - '
'.format( - border=self.border, cls=" ".join(_classes), id_section=id_section - ), + f'
', indent, ) diff --git a/pandas/io/formats/info.py b/pandas/io/formats/info.py new file mode 100644 index 0000000000000..0c08065f55273 --- /dev/null +++ b/pandas/io/formats/info.py @@ -0,0 +1,288 @@ +import sys + +from pandas._config import get_option + +from pandas.io.formats import format as fmt +from pandas.io.formats.printing import pprint_thing + + +def _put_str(s, space): + return str(s)[:space].ljust(space) + + +def info( + data, verbose=None, buf=None, max_cols=None, memory_usage=None, null_counts=None +) -> None: + """ + Print a concise summary of a DataFrame. + + This method prints information about a DataFrame including + the index dtype and column dtypes, non-null values and memory usage. + + Parameters + ---------- + data : DataFrame + DataFrame to print information about. + verbose : bool, optional + Whether to print the full summary. By default, the setting in + ``pandas.options.display.max_info_columns`` is followed. + buf : writable buffer, defaults to sys.stdout + Where to send the output. By default, the output is printed to + sys.stdout. Pass a writable buffer if you need to further process + the output. + max_cols : int, optional + When to switch from the verbose to the truncated output. If the + DataFrame has more than `max_cols` columns, the truncated output + is used. By default, the setting in + ``pandas.options.display.max_info_columns`` is used. + memory_usage : bool, str, optional + Specifies whether total memory usage of the DataFrame + elements (including the index) should be displayed. By default, + this follows the ``pandas.options.display.memory_usage`` setting. + + True always show memory usage. False never shows memory usage. + A value of 'deep' is equivalent to "True with deep introspection". + Memory usage is shown in human-readable units (base-2 + representation). Without deep introspection a memory estimation is + made based in column dtype and number of rows assuming values + consume the same memory amount for corresponding dtypes. With deep + memory introspection, a real memory usage calculation is performed + at the cost of computational resources. + null_counts : bool, optional + Whether to show the non-null counts. By default, this is shown + only if the frame is smaller than + ``pandas.options.display.max_info_rows`` and + ``pandas.options.display.max_info_columns``. A value of True always + shows the counts, and False never shows the counts. + + Returns + ------- + None + This method prints a summary of a DataFrame and returns None. + + See Also + -------- + DataFrame.describe: Generate descriptive statistics of DataFrame + columns. + DataFrame.memory_usage: Memory usage of DataFrame columns. + + Examples + -------- + >>> int_values = [1, 2, 3, 4, 5] + >>> text_values = ['alpha', 'beta', 'gamma', 'delta', 'epsilon'] + >>> float_values = [0.0, 0.25, 0.5, 0.75, 1.0] + >>> df = pd.DataFrame({"int_col": int_values, "text_col": text_values, + ... "float_col": float_values}) + >>> df + int_col text_col float_col + 0 1 alpha 0.00 + 1 2 beta 0.25 + 2 3 gamma 0.50 + 3 4 delta 0.75 + 4 5 epsilon 1.00 + + Prints information of all columns: + + >>> df.info(verbose=True) + + RangeIndex: 5 entries, 0 to 4 + Data columns (total 3 columns): + # Column Non-Null Count Dtype + --- ------ -------------- ----- + 0 int_col 5 non-null int64 + 1 text_col 5 non-null object + 2 float_col 5 non-null float64 + dtypes: float64(1), int64(1), object(1) + memory usage: 248.0+ bytes + + Prints a summary of columns count and its dtypes but not per column + information: + + >>> df.info(verbose=False) + + RangeIndex: 5 entries, 0 to 4 + Columns: 3 entries, int_col to float_col + dtypes: float64(1), int64(1), object(1) + memory usage: 248.0+ bytes + + Pipe output of DataFrame.info to buffer instead of sys.stdout, get + buffer content and writes to a text file: + + >>> import io + >>> buffer = io.StringIO() + >>> df.info(buf=buffer) + >>> s = buffer.getvalue() + >>> with open("df_info.txt", "w", + ... encoding="utf-8") as f: # doctest: +SKIP + ... f.write(s) + 260 + + The `memory_usage` parameter allows deep introspection mode, specially + useful for big DataFrames and fine-tune memory optimization: + + >>> random_strings_array = np.random.choice(['a', 'b', 'c'], 10 ** 6) + >>> df = pd.DataFrame({ + ... 'column_1': np.random.choice(['a', 'b', 'c'], 10 ** 6), + ... 'column_2': np.random.choice(['a', 'b', 'c'], 10 ** 6), + ... 'column_3': np.random.choice(['a', 'b', 'c'], 10 ** 6) + ... }) + >>> df.info() + + RangeIndex: 1000000 entries, 0 to 999999 + Data columns (total 3 columns): + # Column Non-Null Count Dtype + --- ------ -------------- ----- + 0 column_1 1000000 non-null object + 1 column_2 1000000 non-null object + 2 column_3 1000000 non-null object + dtypes: object(3) + memory usage: 22.9+ MB + + >>> df.info(memory_usage='deep') + + RangeIndex: 1000000 entries, 0 to 999999 + Data columns (total 3 columns): + # Column Non-Null Count Dtype + --- ------ -------------- ----- + 0 column_1 1000000 non-null object + 1 column_2 1000000 non-null object + 2 column_3 1000000 non-null object + dtypes: object(3) + memory usage: 188.8 MB + """ + if buf is None: # pragma: no cover + buf = sys.stdout + + lines = [] + + lines.append(str(type(data))) + lines.append(data.index._summary()) + + if len(data.columns) == 0: + lines.append(f"Empty {type(data).__name__}") + fmt.buffer_put_lines(buf, lines) + return + + cols = data.columns + col_count = len(data.columns) + + # hack + if max_cols is None: + max_cols = get_option("display.max_info_columns", len(data.columns) + 1) + + max_rows = get_option("display.max_info_rows", len(data) + 1) + + if null_counts is None: + show_counts = (col_count <= max_cols) and (len(data) < max_rows) + else: + show_counts = null_counts + exceeds_info_cols = col_count > max_cols + + def _verbose_repr(): + lines.append(f"Data columns (total {len(data.columns)} columns):") + + id_head = " # " + column_head = "Column" + col_space = 2 + + max_col = max(len(pprint_thing(k)) for k in cols) + len_column = len(pprint_thing(column_head)) + space = max(max_col, len_column) + col_space + + max_id = len(pprint_thing(col_count)) + len_id = len(pprint_thing(id_head)) + space_num = max(max_id, len_id) + col_space + + header = _put_str(id_head, space_num) + _put_str(column_head, space) + if show_counts: + counts = data.count() + if len(cols) != len(counts): # pragma: no cover + raise AssertionError( + f"Columns must equal counts ({len(cols)} != {len(counts)})" + ) + count_header = "Non-Null Count" + len_count = len(count_header) + non_null = " non-null" + max_count = max(len(pprint_thing(k)) for k in counts) + len(non_null) + space_count = max(len_count, max_count) + col_space + count_temp = "{count}" + non_null + else: + count_header = "" + space_count = len(count_header) + len_count = space_count + count_temp = "{count}" + + dtype_header = "Dtype" + len_dtype = len(dtype_header) + max_dtypes = max(len(pprint_thing(k)) for k in data.dtypes) + space_dtype = max(len_dtype, max_dtypes) + header += _put_str(count_header, space_count) + _put_str( + dtype_header, space_dtype + ) + + lines.append(header) + lines.append( + _put_str("-" * len_id, space_num) + + _put_str("-" * len_column, space) + + _put_str("-" * len_count, space_count) + + _put_str("-" * len_dtype, space_dtype) + ) + + for i, col in enumerate(data.columns): + dtype = data.dtypes.iloc[i] + col = pprint_thing(col) + + line_no = _put_str(f" {i}", space_num) + count = "" + if show_counts: + count = counts.iloc[i] + + lines.append( + line_no + + _put_str(col, space) + + _put_str(count_temp.format(count=count), space_count) + + _put_str(dtype, space_dtype) + ) + + def _non_verbose_repr(): + lines.append(data.columns._summary(name="Columns")) + + def _sizeof_fmt(num, size_qualifier): + # returns size in human readable format + for x in ["bytes", "KB", "MB", "GB", "TB"]: + if num < 1024.0: + return f"{num:3.1f}{size_qualifier} {x}" + num /= 1024.0 + return f"{num:3.1f}{size_qualifier} PB" + + if verbose: + _verbose_repr() + elif verbose is False: # specifically set to False, not nesc None + _non_verbose_repr() + else: + if exceeds_info_cols: + _non_verbose_repr() + else: + _verbose_repr() + + counts = data._data.get_dtype_counts() + dtypes = [f"{k[0]}({k[1]:d})" for k in sorted(counts.items())] + lines.append(f"dtypes: {', '.join(dtypes)}") + + if memory_usage is None: + memory_usage = get_option("display.memory_usage") + if memory_usage: + # append memory usage of df to display + size_qualifier = "" + if memory_usage == "deep": + deep = True + else: + # size_qualifier is just a best effort; not guaranteed to catch + # all cases (e.g., it misses categorical data even with object + # categories) + deep = False + if "object" in counts or data.index._is_memory_usage_qualified(): + size_qualifier = "+" + mem_usage = data.memory_usage(index=True, deep=deep).sum() + lines.append(f"memory usage: {_sizeof_fmt(mem_usage, size_qualifier)}\n") + fmt.buffer_put_lines(buf, lines) diff --git a/pandas/io/formats/latex.py b/pandas/io/formats/latex.py index 8ab56437d5c05..3a3ca84642d51 100644 --- a/pandas/io/formats/latex.py +++ b/pandas/io/formats/latex.py @@ -56,13 +56,12 @@ def write_result(self, buf: IO[str]) -> None: Render a DataFrame to a LaTeX tabular, longtable, or table/tabular environment output. """ - # string representation of the columns if len(self.frame.columns) == 0 or len(self.frame.index) == 0: - info_line = "Empty {name}\nColumns: {col}\nIndex: {idx}".format( - name=type(self.frame).__name__, - col=self.frame.columns, - idx=self.frame.index, + info_line = ( + f"Empty {type(self.frame).__name__}\n" + f"Columns: {self.frame.columns}\n" + f"Index: {self.frame.index}" ) strcols = [[info_line]] else: @@ -141,8 +140,8 @@ def pad_empties(x): buf.write("\\endhead\n") buf.write("\\midrule\n") buf.write( - "\\multicolumn{{{n}}}{{r}}{{{{Continued on next " - "page}}}} \\\\\n".format(n=len(row)) + f"\\multicolumn{{{len(row)}}}{{r}}" + "{{Continued on next page}} \\\\\n" ) buf.write("\\midrule\n") buf.write("\\endfoot\n\n") @@ -172,7 +171,7 @@ def pad_empties(x): if self.bold_rows and self.fmt.index: # bold row labels crow = [ - "\\textbf{{{x}}}".format(x=x) + f"\\textbf{{{x}}}" if j < ilevels and x.strip() not in ["", "{}"] else x for j, x in enumerate(crow) @@ -211,9 +210,8 @@ def append_col(): # write multicolumn if needed if ncol > 1: row2.append( - "\\multicolumn{{{ncol:d}}}{{{fmt:s}}}{{{txt:s}}}".format( - ncol=ncol, fmt=self.multicolumn_format, txt=coltext.strip() - ) + f"\\multicolumn{{{ncol:d}}}{{{self.multicolumn_format}}}" + f"{{{coltext.strip()}}}" ) # don't modify where not needed else: @@ -256,9 +254,7 @@ def _format_multirow( break if nrow > 1: # overwrite non-multirow entry - row[j] = "\\multirow{{{nrow:d}}}{{*}}{{{row:s}}}".format( - nrow=nrow, row=row[j].strip() - ) + row[j] = f"\\multirow{{{nrow:d}}}{{*}}{{{row[j].strip()}}}" # save when to end the current block with \cline self.clinebuf.append([i + nrow - 1, j + 1]) return row @@ -269,7 +265,7 @@ def _print_cline(self, buf: IO[str], i: int, icol: int) -> None: """ for cl in self.clinebuf: if cl[0] == i: - buf.write("\\cline{{{cl:d}-{icol:d}}}\n".format(cl=cl[1], icol=icol)) + buf.write(f"\\cline{{{cl[1]:d}-{icol:d}}}\n") # remove entries that have been written to buffer self.clinebuf = [x for x in self.clinebuf if x[0] != i] @@ -293,19 +289,19 @@ def _write_tabular_begin(self, buf, column_format: str): if self.caption is None: caption_ = "" else: - caption_ = "\n\\caption{{{}}}".format(self.caption) + caption_ = f"\n\\caption{{{self.caption}}}" if self.label is None: label_ = "" else: - label_ = "\n\\label{{{}}}".format(self.label) + label_ = f"\n\\label{{{self.label}}}" - buf.write("\\begin{{table}}\n\\centering{}{}\n".format(caption_, label_)) + buf.write(f"\\begin{{table}}\n\\centering{caption_}{label_}\n") else: # then write output only in a tabular environment pass - buf.write("\\begin{{tabular}}{{{fmt}}}\n".format(fmt=column_format)) + buf.write(f"\\begin{{tabular}}{{{column_format}}}\n") def _write_tabular_end(self, buf): """ @@ -341,18 +337,18 @@ def _write_longtable_begin(self, buf, column_format: str): `__ e.g 'rcl' for 3 columns """ - buf.write("\\begin{{longtable}}{{{fmt}}}\n".format(fmt=column_format)) + buf.write(f"\\begin{{longtable}}{{{column_format}}}\n") if self.caption is not None or self.label is not None: if self.caption is None: pass else: - buf.write("\\caption{{{}}}".format(self.caption)) + buf.write(f"\\caption{{{self.caption}}}") if self.label is None: pass else: - buf.write("\\label{{{}}}".format(self.label)) + buf.write(f"\\label{{{self.label}}}") # a double-backslash is required at the end of the line # as discussed here: diff --git a/pandas/io/formats/printing.py b/pandas/io/formats/printing.py index 13b18a0b5fb6f..36e774305b577 100644 --- a/pandas/io/formats/printing.py +++ b/pandas/io/formats/printing.py @@ -229,7 +229,7 @@ def as_escaped_string( max_seq_items=max_seq_items, ) elif isinstance(thing, str) and quote_strings: - result = "'{thing}'".format(thing=as_escaped_string(thing)) + result = f"'{as_escaped_string(thing)}'" else: result = as_escaped_string(thing) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 565752e269d79..018441dacd9a8 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -85,7 +85,7 @@ class Styler: number and ```` is the column number. na_rep : str, optional Representation for missing values. - If ``na_rep`` is None, no special formatting is applied + If ``na_rep`` is None, no special formatting is applied. .. versionadded:: 1.0.0 @@ -446,13 +446,13 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style Parameters ---------- formatter : str, callable, dict or None - If ``formatter`` is None, the default formatter is used + If ``formatter`` is None, the default formatter is used. subset : IndexSlice An argument to ``DataFrame.loc`` that restricts which elements ``formatter`` is applied to. na_rep : str, optional Representation for missing values. - If ``na_rep`` is None, no special formatting is applied + If ``na_rep`` is None, no special formatting is applied. .. versionadded:: 1.0.0 @@ -462,7 +462,6 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style Notes ----- - ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where ``a`` is one of @@ -474,7 +473,6 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style Examples -------- - >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b']) >>> df.style.format("{:.2%}") >>> df['c'] = ['a', 'b', 'c', 'd'] @@ -802,7 +800,6 @@ def where( -------- Styler.applymap """ - if other is None: other = "" diff --git a/pandas/io/html.py b/pandas/io/html.py index c676bfb1f0c74..561570f466b68 100644 --- a/pandas/io/html.py +++ b/pandas/io/html.py @@ -395,7 +395,6 @@ def _parse_thead_tbody_tfoot(self, table_html): - Move rows from bottom of body to footer only if all elements inside row are
""" - header_rows = self._parse_thead_tr(table_html) body_rows = self._parse_tbody_tr(table_html) footer_rows = self._parse_tfoot_tr(table_html) @@ -435,7 +434,6 @@ def _expand_colspan_rowspan(self, rows): Any cell with ``rowspan`` or ``colspan`` will have its contents copied to subsequent cells. """ - all_texts = [] # list of rows, each a list of str remainder = [] # list of (index, text, nrows) @@ -602,7 +600,8 @@ def _build_doc(self): def _build_xpath_expr(attrs) -> str: - """Build an xpath expression to simulate bs4's ability to pass in kwargs to + """ + Build an xpath expression to simulate bs4's ability to pass in kwargs to search for attributes when using the lxml parser. Parameters diff --git a/pandas/io/json/_json.py b/pandas/io/json/_json.py index 04fd17a00041b..77a0c2f99496b 100644 --- a/pandas/io/json/_json.py +++ b/pandas/io/json/_json.py @@ -266,7 +266,6 @@ def __init__( to know what the index is, forces orient to records, and forces date_format to 'iso'. """ - super().__init__( obj, orient, @@ -526,7 +525,6 @@ def read_json( Examples -------- - >>> df = pd.DataFrame([['a', 'b'], ['c', 'd']], ... index=['row 1', 'row 2'], ... columns=['col 1', 'col 2']) @@ -572,7 +570,6 @@ def read_json( "data": [{"index": "row 1", "col 1": "a", "col 2": "b"}, {"index": "row 2", "col 1": "c", "col 2": "d"}]}' """ - if orient == "table" and dtype: raise ValueError("cannot pass both dtype and orient='table'") if orient == "table" and convert_axes: @@ -886,7 +883,6 @@ def _try_convert_data(self, name, data, use_dtypes=True, convert_dates=True): """ Try to parse a ndarray like into a column by inferring dtype. """ - # don't try to coerce, unless a force conversion if use_dtypes: if not self.dtype: @@ -963,7 +959,6 @@ def _try_convert_to_date(self, data): Try to coerce object in epoch/iso formats and integer/float in epoch formats. Return a boolean if parsing was successful. """ - # no conversion on empty if not len(data): return data, False @@ -1117,7 +1112,6 @@ def _process_converter(self, f, filt=None): """ Take a conversion function and possibly recreate the frame. """ - if filt is None: filt = lambda col, c: True diff --git a/pandas/io/json/_normalize.py b/pandas/io/json/_normalize.py index b638bdc0bc1eb..f158ad6cd89e3 100644 --- a/pandas/io/json/_normalize.py +++ b/pandas/io/json/_normalize.py @@ -18,7 +18,6 @@ def convert_to_line_delimits(s): """ Helper function that converts JSON lists to line delimited JSON. """ - # Determine we have a JSON list to turn to lines otherwise just return the # json object, only lists can if not s[0] == "[" and s[-1] == "]": @@ -62,7 +61,6 @@ def nested_to_record( Examples -------- - IN[52]: nested_to_record(dict(flat1=1,dict1=dict(c=1,d=2), nested=dict(e=dict(c=1,d=2),d=2))) Out[52]: @@ -161,12 +159,10 @@ def _json_normalize( Examples -------- - - >>> from pandas.io.json import json_normalize >>> data = [{'id': 1, 'name': {'first': 'Coleen', 'last': 'Volk'}}, ... {'name': {'given': 'Mose', 'family': 'Regner'}}, ... {'id': 2, 'name': 'Faye Raker'}] - >>> json_normalize(data) + >>> pandas.json_normalize(data) id name name.family name.first name.given name.last 0 1.0 NaN NaN Coleen NaN Volk 1 NaN NaN Regner NaN Mose NaN diff --git a/pandas/io/orc.py b/pandas/io/orc.py index bbefe447cb7fe..ea79efd0579e5 100644 --- a/pandas/io/orc.py +++ b/pandas/io/orc.py @@ -42,7 +42,6 @@ def read_orc( ------- DataFrame """ - # we require a newer version of pyarrow than we support for parquet import pyarrow diff --git a/pandas/io/parquet.py b/pandas/io/parquet.py index 926635062d853..9ae9729fc05ee 100644 --- a/pandas/io/parquet.py +++ b/pandas/io/parquet.py @@ -13,7 +13,6 @@ def get_engine(engine: str) -> "BaseImpl": """ return our implementation """ - if engine == "auto": engine = get_option("io.parquet.engine") @@ -297,6 +296,5 @@ def read_parquet(path, engine: str = "auto", columns=None, **kwargs): ------- DataFrame """ - impl = get_engine(engine) return impl.read(path, columns=columns, **kwargs) diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py index 8bc8470ae7658..1cbc518f69e6b 100755 --- a/pandas/io/parsers.py +++ b/pandas/io/parsers.py @@ -6,10 +6,11 @@ import csv import datetime from io import BufferedIOBase, RawIOBase, StringIO, TextIOWrapper +from itertools import chain import re import sys from textwrap import fill -from typing import Any, Dict, Set +from typing import Any, Dict, List, Set import warnings import numpy as np @@ -89,7 +90,7 @@ ---------- filepath_or_buffer : str, path object or file-like object Any valid string path is acceptable. The string could be a URL. Valid - URL schemes include http, ftp, s3, and file. For file URLs, a host is + URL schemes include http, ftp, s3, gs, and file. For file URLs, a host is expected. A local file could be: file://localhost/path/to/table.csv. If you want to pass in a path object, pandas accepts any ``os.PathLike``. @@ -407,7 +408,6 @@ def _validate_names(names): ValueError If names are not unique. """ - if names is not None: if len(names) != len(set(names)): raise ValueError("Duplicate names are not allowed.") @@ -705,7 +705,6 @@ def read_fwf( infer_nrows=100, **kwds, ): - r""" Read a table of fixed-width formatted lines into DataFrame. @@ -761,7 +760,6 @@ def read_fwf( -------- >>> pd.read_fwf('data.csv') # doctest: +SKIP """ - # Check input arguments. if colspecs is None and widths is None: raise ValueError("Must specify either colspecs or widths") @@ -1253,7 +1251,6 @@ def _validate_skipfooter_arg(skipfooter): ------ ValueError : 'skipfooter' was not a non-negative integer. """ - if not is_integer(skipfooter): raise ValueError("skipfooter must be an integer") @@ -1423,6 +1420,56 @@ def __init__(self, kwds): # keep references to file handles opened by the parser itself self.handles = [] + def _validate_parse_dates_presence(self, columns: List[str]): + """ + Check if parse_dates are in columns. + + if user has provided names for parse_dates, check if those columns + are available. + + Parameters + ---------- + columns : list + list of names of the dataframe. + + Raises + ------ + ValueError + If column to parse_date is not in dataframe. + + """ + if isinstance(self.parse_dates, list): + # a column in parse_dates could be represented + # ColReference = Union[int, str] + # DateGroups = List[ColReference] + # ParseDates = Union[ DateGroups, List[DateGroups], + # Dict[ColReference, DateGroups]] + cols_needed = [] + for col in self.parse_dates: + if isinstance(col, list): + cols_needed.extend(col) + else: + cols_needed.append(col) + elif isinstance(self.parse_dates, dict): + cols_needed = list(chain(*self.parse_dates.values())) + else: + cols_needed = [] + + # get only columns that are references using names (str), not by index + missing_cols = ", ".join( + sorted( + { + col + for col in cols_needed + if isinstance(col, str) and col not in columns + } + ) + ) + if missing_cols: + raise ValueError( + f"Missing column provided to 'parse_dates': '{missing_cols}'" + ) + def close(self): for f in self.handles: f.close() @@ -1457,8 +1504,10 @@ def _should_parse_dates(self, i): def _extract_multi_indexer_columns( self, header, index_names, col_names, passed_names=False ): - """ extract and return the names, index_names, col_names - header is a list-of-lists returned from the parsers """ + """ + extract and return the names, index_names, col_names + header is a list-of-lists returned from the parsers + """ if len(header) < 2: return header[0], index_names, col_names, passed_names @@ -1492,11 +1541,10 @@ def extract(r): # level, then our header was too long. for n in range(len(columns[0])): if all(ensure_str(col[n]) in self.unnamed_cols for col in columns): + header = ",".join(str(x) for x in self.header) raise ParserError( - "Passed header=[{header}] are too many rows for this " - "multi_index of columns".format( - header=",".join(str(x) for x in self.header) - ) + f"Passed header=[{header}] are too many rows " + "for this multi_index of columns" ) # Clean the column names (if we have an index_col). @@ -1795,7 +1843,6 @@ def _cast_types(self, values, cast_type, column): ------- converted : ndarray """ - if is_categorical_dtype(cast_type): known_cats = ( isinstance(cast_type, CategoricalDtype) @@ -1942,6 +1989,7 @@ def __init__(self, src, **kwds): if len(self.names) < len(usecols): _validate_usecols_names(usecols, self.names) + self._validate_parse_dates_presence(self.names) self._set_noconvert_columns() self.orig_names = self.names @@ -2312,6 +2360,7 @@ def __init__(self, f, **kwds): if self.index_names is None: self.index_names = index_names + self._validate_parse_dates_presence(self.columns) if self.parse_dates: self._no_thousands_columns = self._set_no_thousands_columns() else: @@ -2488,7 +2537,7 @@ def get_chunk(self, size=None): def _convert_data(self, data): # apply converters def _clean_mapping(mapping): - "converts col numbers to names" + """converts col numbers to names""" clean = {} for col, v in mapping.items(): if isinstance(col, int) and col not in self.orig_names: @@ -2863,7 +2912,6 @@ def _alert_malformed(self, msg, row_num): Because this row number is displayed, we 1-index, even though we 0-index internally. """ - if self.error_bad_lines: raise ParserError(msg) elif self.warn_bad_lines: @@ -2882,7 +2930,6 @@ def _next_iter_line(self, row_num): ---------- row_num : The row number of the line being parsed. """ - try: return next(self.data) except csv.Error as e: @@ -2942,7 +2989,6 @@ def _remove_empty_lines(self, lines): filtered_lines : array-like The same array of lines with the "empty" ones removed. """ - ret = [] for l in lines: # Remove empty lines and lines with only one whitespace value @@ -3514,7 +3560,6 @@ def _get_na_values(col, na_values, na_fvalues, keep_default_na): 1) na_values : the string NaN values for that column. 2) na_fvalues : the float NaN values for that column. """ - if isinstance(na_values, dict): if col in na_values: return na_values[col], na_fvalues[col] @@ -3613,8 +3658,8 @@ def get_rows(self, infer_nrows, skiprows=None): def detect_colspecs(self, infer_nrows=100, skiprows=None): # Regex escape the delimiters - delimiters = "".join(r"\{}".format(x) for x in self.delimiter) - pattern = re.compile("([^{}]+)".format(delimiters)) + delimiters = "".join(fr"\{x}" for x in self.delimiter) + pattern = re.compile(f"([^{delimiters}]+)") rows = self.get_rows(infer_nrows, skiprows) if not rows: raise EmptyDataError("No rows from which to infer column width") diff --git a/pandas/io/pickle.py b/pandas/io/pickle.py index e51f24b551f31..4e731b8ecca11 100644 --- a/pandas/io/pickle.py +++ b/pandas/io/pickle.py @@ -171,21 +171,22 @@ def read_pickle( # 1) try standard library Pickle # 2) try pickle_compat (older pandas version) to handle subclass changes - - excs_to_catch = (AttributeError, ImportError, ModuleNotFoundError) + # 3) try pickle_compat with latin-1 encoding upon a UnicodeDecodeError try: - with warnings.catch_warnings(record=True): - # We want to silence any warnings about, e.g. moved modules. - warnings.simplefilter("ignore", Warning) - return pickle.load(f) - except excs_to_catch: - # e.g. - # "No module named 'pandas.core.sparse.series'" - # "Can't get attribute '__nat_unpickle' on >> df.to_hdf('./store.h5', 'data') >>> reread = pd.read_hdf('./store.h5') """ - if mode not in ["r", "r+", "a"]: raise ValueError( f"mode {mode} is not allowed while performing a read. " @@ -569,9 +566,10 @@ def __getattr__(self, name: str): ) def __contains__(self, key: str) -> bool: - """ check for existence of this key - can match the exact pathname or the pathnm w/o the leading '/' - """ + """ + check for existence of this key + can match the exact pathname or the pathnm w/o the leading '/' + """ node = self.get_node(key) if node is not None: name = node._v_pathname @@ -902,7 +900,6 @@ def select_as_multiple( raises TypeError if keys is not a list or tuple raises ValueError if the tables are not ALL THE SAME DIMENSIONS """ - # default to single select where = _ensure_term(where, scope_level=1) if isinstance(keys, (list, tuple)) and len(keys) == 1: @@ -1139,11 +1136,11 @@ def append( queries, or True to use all columns. By default only the axes of the object are indexed. See `here `__. - min_itemsize : dict of columns that specify minimum string sizes - nan_rep : string to use as string nan representation + min_itemsize : dict of columns that specify minimum str sizes + nan_rep : str to use as str nan representation chunksize : size to chunk the writing expectedrows : expected TOTAL row size of this table - encoding : default None, provide an encoding for strings + encoding : default None, provide an encoding for str dropna : bool, default False Do not write an ALL nan row to the store settable by the option 'io.hdf.dropna_table'. @@ -1302,7 +1299,6 @@ def create_table_index( ------ TypeError: raises if the node is not a table """ - # version requirements _tables() s = self.get_storer(key) @@ -1522,7 +1518,6 @@ def _check_if_open(self): def _validate_format(self, format: str) -> str: """ validate / deprecate formats """ - # validate try: format = _FORMAT_MAP[format.lower()] @@ -1540,7 +1535,6 @@ def _create_storer( errors: str = "strict", ) -> Union["GenericFixed", "Table"]: """ return a suitable class to operate """ - cls: Union[Type["GenericFixed"], Type["Table"]] if value is not None and not isinstance(value, (Series, DataFrame)): @@ -1831,18 +1825,18 @@ def get_result(self, coordinates: bool = False): class IndexCol: - """ an index column description class - - Parameters - ---------- + """ + an index column description class - axis : axis which I reference - values : the ndarray like converted values - kind : a string description of this type - typ : the pytables type - pos : the position in the pytables + Parameters + ---------- + axis : axis which I reference + values : the ndarray like converted values + kind : a string description of this type + typ : the pytables type + pos : the position in the pytables - """ + """ is_an_indexable = True is_data_indexable = True @@ -1999,9 +1993,11 @@ def __iter__(self): return iter(self.values) def maybe_set_size(self, min_itemsize=None): - """ maybe set a string col itemsize: - min_itemsize can be an integer or a dict with this columns name - with an integer size """ + """ + maybe set a string col itemsize: + min_itemsize can be an integer or a dict with this columns name + with an integer size + """ if _ensure_decoded(self.kind) == "string": if isinstance(min_itemsize, dict): @@ -2023,7 +2019,6 @@ def validate_and_set(self, handler: "AppendableTable", append: bool): def validate_col(self, itemsize=None): """ validate this column: return the compared against itemsize """ - # validate this column for string truncation (or reset to the max size) if _ensure_decoded(self.kind) == "string": c = self.col @@ -2051,9 +2046,10 @@ def validate_attr(self, append: bool): ) def update_info(self, info): - """ set/update the info for this indexable with the key/value - if there is a conflict raise/warn as needed """ - + """ + set/update the info for this indexable with the key/value + if there is a conflict raise/warn as needed + """ for key in self._info_fields: value = getattr(self, key, None) @@ -2140,17 +2136,17 @@ def set_attr(self): class DataCol(IndexCol): - """ a data holding column, by definition this is not indexable - - Parameters - ---------- + """ + a data holding column, by definition this is not indexable - data : the actual data - cname : the column name in the table to hold the data (typically - values) - meta : a string description of the metadata - metadata : the actual metadata - """ + Parameters + ---------- + data : the actual data + cname : the column name in the table to hold the data (typically + values) + meta : a string description of the metadata + metadata : the actual metadata + """ is_an_indexable = False is_data_indexable = False @@ -2235,7 +2231,6 @@ def _get_atom(cls, values: Union[np.ndarray, ABCExtensionArray]) -> "Col": """ Get an appropriately typed and shaped pytables.Col object for values. """ - dtype = values.dtype itemsize = dtype.itemsize @@ -2460,16 +2455,17 @@ class GenericDataIndexableCol(DataIndexableCol): class Fixed: - """ represent an object in my store - facilitate read/write of various types of objects - this is an abstract base class + """ + represent an object in my store + facilitate read/write of various types of objects + this is an abstract base class - Parameters - ---------- - parent : HDFStore - group : Node - The group node where the table resides. - """ + Parameters + ---------- + parent : HDFStore + group : Node + The group node where the table resides. + """ pandas_kind: str format_type: str = "fixed" # GH#30962 needed by dask @@ -2596,9 +2592,10 @@ def validate_version(self, where=None): return True def infer_axes(self): - """ infer the axes of my storer - return a boolean indicating if we have a valid storer or not """ - + """ + infer the axes of my storer + return a boolean indicating if we have a valid storer or not + """ s = self.storable if s is None: return False @@ -2722,7 +2719,7 @@ def read_array( if isinstance(node, tables.VLArray): ret = node[0][start:stop] else: - dtype = getattr(attrs, "value_type", None) + dtype = _ensure_decoded(getattr(attrs, "value_type", None)) shape = getattr(attrs, "shape", None) if shape is not None: @@ -2884,7 +2881,6 @@ def read_index_node( def write_array_empty(self, key: str, value: ArrayLike): """ write a 0-len array """ - # ugly hack for length 0 axes arr = np.empty((1,) * value.ndim) self._handle.create_array(self.group, key, arr) @@ -3085,9 +3081,8 @@ def write(self, obj, **kwargs): self.attrs.ndim = data.ndim for i, ax in enumerate(data.axes): - if i == 0: - if not ax.is_unique: - raise ValueError("Columns index has to be unique for fixed format") + if i == 0 and (not ax.is_unique): + raise ValueError("Columns index has to be unique for fixed format") self.write_index(f"axis{i}", ax) # Supporting mixed-type DataFrame objects...nontrivial @@ -3105,29 +3100,29 @@ class FrameFixed(BlockManagerFixed): class Table(Fixed): - """ represent a table: - facilitate read/write of various types of tables - - Attrs in Table Node - ------------------- - These are attributes that are store in the main table node, they are - necessary to recreate these tables when read back in. - - index_axes : a list of tuples of the (original indexing axis and - index column) - non_index_axes: a list of tuples of the (original index axis and - columns on a non-indexing axis) - values_axes : a list of the columns which comprise the data of this - table - data_columns : a list of the columns that we are allowing indexing - (these become single columns in values_axes), or True to force all - columns - nan_rep : the string to use for nan representations for string - objects - levels : the names of levels - metadata : the names of the metadata columns - - """ + """ + represent a table: + facilitate read/write of various types of tables + + Attrs in Table Node + ------------------- + These are attributes that are store in the main table node, they are + necessary to recreate these tables when read back in. + + index_axes : a list of tuples of the (original indexing axis and + index column) + non_index_axes: a list of tuples of the (original index axis and + columns on a non-indexing axis) + values_axes : a list of the columns which comprise the data of this + table + data_columns : a list of the columns that we are allowing indexing + (these become single columns in values_axes), or True to force all + columns + nan_rep : the string to use for nan representations for string + objects + levels : the names of levels + metadata : the names of the metadata columns + """ pandas_kind = "wide_table" format_type: str = "table" # GH#30962 needed by dask @@ -3229,7 +3224,8 @@ def is_multi_index(self) -> bool: return isinstance(self.levels, list) def validate_multiindex(self, obj): - """validate that we can store the multi-index; reset and return the + """ + validate that we can store the multi-index; reset and return the new object """ levels = [ @@ -3294,7 +3290,6 @@ def data_orientation(self): def queryables(self) -> Dict[str, Any]: """ return a dict of the kinds allowable columns for this object """ - # mypy doesn't recognize DataFrame._AXIS_NAMES, so we re-write it here axis_names = {0: "index", 1: "columns"} @@ -3381,7 +3376,8 @@ def validate_version(self, where=None): warnings.warn(ws, IncompatibilityWarning) def validate_min_itemsize(self, min_itemsize): - """validate the min_itemsize doesn't contain items that are not in the + """ + validate the min_itemsize doesn't contain items that are not in the axes this needs data_columns to be defined """ if min_itemsize is None: @@ -3503,7 +3499,6 @@ def create_index(self, columns=None, optlevel=None, kind: Optional[str] = None): Cannot index Time64Col or ComplexCol. Pytables must be >= 3.0. """ - if not self.infer_axes(): return if columns is False: @@ -3570,7 +3565,6 @@ def _read_axes( ------- List[Tuple[index_values, column_values]] """ - # create the selection selection = Selection(self, where=where, start=start, stop=stop) values = selection.select() @@ -3595,10 +3589,10 @@ def get_object(cls, obj, transposed: bool): return obj def validate_data_columns(self, data_columns, min_itemsize, non_index_axes): - """take the input data_columns and min_itemize and create a data + """ + take the input data_columns and min_itemize and create a data columns spec """ - if not len(non_index_axes): return [] @@ -3665,7 +3659,6 @@ def _create_axes( min_itemsize: Dict[str, int] or None, default None The min itemsize for a column in bytes. """ - if not isinstance(obj, DataFrame): group = self.group._v_name raise TypeError( @@ -3919,7 +3912,6 @@ def get_blk_items(mgr, blocks): def process_axes(self, obj, selection: "Selection", columns=None): """ process axes filters """ - # make a copy to avoid side effects if columns is not None: columns = list(columns) @@ -3984,7 +3976,6 @@ def create_description( expectedrows: Optional[int], ) -> Dict[str, Any]: """ create the description of the table from the axes & values """ - # provided expected rows if its passed if expectedrows is None: expectedrows = max(self.nrows_expected, 10000) @@ -4011,10 +4002,10 @@ def create_description( def read_coordinates( self, where=None, start: Optional[int] = None, stop: Optional[int] = None, ): - """select coordinates (row numbers) from a table; return the + """ + select coordinates (row numbers) from a table; return the coordinates object """ - # validate the version self.validate_version(where) @@ -4041,10 +4032,10 @@ def read_column( start: Optional[int] = None, stop: Optional[int] = None, ): - """return a single column from the table, generally only indexables + """ + return a single column from the table, generally only indexables are interesting """ - # validate the version self.validate_version() @@ -4080,10 +4071,11 @@ def read_column( class WORMTable(Table): - """ a write-once read-many table: this format DOES NOT ALLOW appending to a - table. writing is a one-time operation the data are stored in a format - that allows for searching the data on disk - """ + """ + a write-once read-many table: this format DOES NOT ALLOW appending to a + table. writing is a one-time operation the data are stored in a format + that allows for searching the data on disk + """ table_type = "worm" @@ -4094,14 +4086,16 @@ def read( start: Optional[int] = None, stop: Optional[int] = None, ): - """ read the indices and the indexing array, calculate offset rows and - return """ + """ + read the indices and the indexing array, calculate offset rows and return + """ raise NotImplementedError("WORMTable needs to implement read") def write(self, **kwargs): - """ write in a format that we can search later on (but cannot append - to): write out the indices and the values using _write_array - (e.g. a CArray) create an indexing table so that we can search + """ + write in a format that we can search later on (but cannot append + to): write out the indices and the values using _write_array + (e.g. a CArray) create an indexing table so that we can search """ raise NotImplementedError("WORMTable needs to implement write") @@ -4170,9 +4164,9 @@ def write( table.write_data(chunksize, dropna=dropna) def write_data(self, chunksize: Optional[int], dropna: bool = False): - """ we form the data into a 2-d including indexes,values,mask - write chunk-by-chunk """ - + """ + we form the data into a 2-d including indexes,values,mask write chunk-by-chunk + """ names = self.dtype.names nrows = self.nrows_expected @@ -4216,7 +4210,7 @@ def write_data(self, chunksize: Optional[int], dropna: bool = False): chunksize = 100000 rows = np.empty(min(chunksize, nrows), dtype=self.dtype) - chunks = int(nrows / chunksize) + 1 + chunks = nrows // chunksize + 1 for i in range(chunks): start_i = i * chunksize end_i = min((i + 1) * chunksize, nrows) @@ -4245,7 +4239,6 @@ def write_data_chunk( mask : an array of the masks values : an array of the values """ - # 0 len for v in values: if not np.prod(v.shape): @@ -4840,7 +4833,6 @@ def _convert_string_array(data: np.ndarray, encoding: str, errors: str) -> np.nd ------- np.ndarray[fixed-length-string] """ - # encode if needed if len(data): data = ( diff --git a/pandas/io/sas/sas7bdat.py b/pandas/io/sas/sas7bdat.py index 9b40778dbcfdf..d47dd2c71b86f 100644 --- a/pandas/io/sas/sas7bdat.py +++ b/pandas/io/sas/sas7bdat.py @@ -120,8 +120,10 @@ def column_data_offsets(self): 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)""" + """ + 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): diff --git a/pandas/io/sas/sas_xport.py b/pandas/io/sas/sas_xport.py index 3cf7fd885e564..e67d68f7e0975 100644 --- a/pandas/io/sas/sas_xport.py +++ b/pandas/io/sas/sas_xport.py @@ -79,12 +79,12 @@ Return XportReader object for reading file incrementally.""" -_read_sas_doc = """Read a SAS file into a DataFrame. +_read_sas_doc = f"""Read a SAS file into a DataFrame. -%(_base_params_doc)s -%(_format_params_doc)s -%(_params2_doc)s -%(_iterator_doc)s +{_base_params_doc} +{_format_params_doc} +{_params2_doc} +{_iterator_doc} Returns ------- @@ -102,19 +102,13 @@ >>> for chunk in itr: >>> do_something(chunk) -""" % { - "_base_params_doc": _base_params_doc, - "_format_params_doc": _format_params_doc, - "_params2_doc": _params2_doc, - "_iterator_doc": _iterator_doc, -} - +""" -_xport_reader_doc = """\ +_xport_reader_doc = f"""\ Class for reading SAS Xport files. -%(_base_params_doc)s -%(_params2_doc)s +{_base_params_doc} +{_params2_doc} Attributes ---------- @@ -122,11 +116,7 @@ Contains information about the file fields : list Contains information about the variables in the file -""" % { - "_base_params_doc": _base_params_doc, - "_params2_doc": _params2_doc, -} - +""" _read_method_doc = """\ Read observations from SAS Xport file, returning as data frame. @@ -185,7 +175,7 @@ def _handle_truncated_float_vec(vec, nbytes): if nbytes != 8: vec1 = np.zeros(len(vec), np.dtype("S8")) - dtype = np.dtype("S%d,S%d" % (nbytes, 8 - nbytes)) + dtype = np.dtype(f"S{nbytes},S{8 - nbytes}") vec2 = vec1.view(dtype=dtype) vec2["f0"] = vec return vec2 @@ -198,7 +188,6 @@ def _parse_float_vec(vec): Parse a vector of float values representing IBM 8 byte floats into native 8 byte floats. """ - dtype = np.dtype(">u4,>u4") vec1 = vec.view(dtype=dtype) xport1 = vec1["f0"] @@ -411,7 +400,6 @@ def _record_count(self) -> int: Side effect: returns file position to record_start. """ - self.filepath_or_buffer.seek(0, 2) total_records_length = self.filepath_or_buffer.tell() - self.record_start diff --git a/pandas/io/spss.py b/pandas/io/spss.py index cdbe14e9fe927..9605faeb36590 100644 --- a/pandas/io/spss.py +++ b/pandas/io/spss.py @@ -20,7 +20,7 @@ def read_spss( Parameters ---------- - path : string or Path + path : str or Path File path. usecols : list-like, optional Return a subset of the columns. If None, return all columns. diff --git a/pandas/io/sql.py b/pandas/io/sql.py index 58fed0d18dd4a..9a53e7cd241e1 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -120,7 +120,6 @@ def _parse_date_columns(data_frame, parse_dates): def _wrap_result(data, columns, index_col=None, coerce_float=True, parse_dates=None): """Wrap result set of query in a DataFrame.""" - frame = DataFrame.from_records(data, columns=columns, coerce_float=coerce_float) frame = _parse_date_columns(frame, parse_dates) @@ -228,7 +227,6 @@ def read_sql_table( -------- >>> pd.read_sql_table('table_name', 'postgres:///db_name') # doctest:+SKIP """ - con = _engine_builder(con) if not _is_sqlalchemy_connectable(con): raise NotImplementedError( @@ -240,8 +238,8 @@ def read_sql_table( meta = MetaData(con, schema=schema) try: meta.reflect(only=[table_name], views=True) - except sqlalchemy.exc.InvalidRequestError: - raise ValueError(f"Table {table_name} not found") + except sqlalchemy.exc.InvalidRequestError as err: + raise ValueError(f"Table {table_name} not found") from err pandas_sql = SQLDatabase(con, meta=meta) table = pandas_sql.read_table( @@ -284,7 +282,7 @@ def read_sql_query( Using SQLAlchemy makes it possible to use any DB supported by that library. If a DBAPI2 object, only sqlite3 is supported. - index_col : str or list of strings, optional, default: None + index_col : str or list of str, optional, default: None Column(s) to set as index(MultiIndex). coerce_float : bool, default True Attempts to convert values of non-string, non-numeric objects (like @@ -358,13 +356,13 @@ def read_sql( sql : str or SQLAlchemy Selectable (select or text object) SQL query to be executed or a table name. con : SQLAlchemy connectable (engine/connection) or database str URI - or DBAPI2 connection (fallback mode)' + or DBAPI2 connection (fallback mode). Using SQLAlchemy makes it possible to use any DB supported by that library. If a DBAPI2 object, only sqlite3 is supported. The user is responsible for engine disposal and connection closure for the SQLAlchemy connectable. See - `here `_ - index_col : str or list of strings, optional, default: None + `here `_. + index_col : str or list of str, optional, default: None Column(s) to set as index(MultiIndex). coerce_float : bool, default True Attempts to convert values of non-string, non-numeric objects (like @@ -655,7 +653,8 @@ def create(self): self._execute_create() def _execute_insert(self, conn, keys, data_iter): - """Execute SQL statement inserting data + """ + Execute SQL statement inserting data Parameters ---------- @@ -669,7 +668,8 @@ def _execute_insert(self, conn, keys, data_iter): conn.execute(self.table.insert(), data) def _execute_insert_multi(self, conn, keys, data_iter): - """Alternative to _execute_insert for DBs support multivalue INSERT. + """ + Alternative to _execute_insert for DBs support multivalue INSERT. Note: multi-value insert is usually faster for analytics DBs and tables containing a few columns @@ -685,7 +685,7 @@ def insert_data(self): try: temp.reset_index(inplace=True) except ValueError as err: - raise ValueError(f"duplicate name in index/columns: {err}") + raise ValueError(f"duplicate name in index/columns: {err}") from err else: temp = self.frame @@ -758,7 +758,6 @@ def _query_iterator( self, result, chunksize, columns, coerce_float=True, parse_dates=None ): """Return generator through chunked result set.""" - while True: data = result.fetchmany(chunksize) if not data: @@ -1095,7 +1094,8 @@ def read_table( schema=None, chunksize=None, ): - """Read SQL database table into a DataFrame. + """ + Read SQL database table into a DataFrame. Parameters ---------- @@ -1149,7 +1149,6 @@ def _query_iterator( result, chunksize, columns, index_col=None, coerce_float=True, parse_dates=None ): """Return generator through chunked result set""" - while True: data = result.fetchmany(chunksize) if not data: @@ -1172,7 +1171,8 @@ def read_query( params=None, chunksize=None, ): - """Read SQL query into a DataFrame. + """ + Read SQL query into a DataFrame. Parameters ---------- @@ -1387,8 +1387,8 @@ def _create_sql_schema(self, frame, table_name, keys=None, dtype=None): def _get_unicode_name(name): try: uname = str(name).encode("utf-8", "strict").decode("utf-8") - except UnicodeError: - raise ValueError(f"Cannot convert identifier to UTF-8: '{name}'") + except UnicodeError as err: + raise ValueError(f"Cannot convert identifier to UTF-8: '{name}'") from err return uname @@ -1440,7 +1440,7 @@ def _execute_create(self): for stmt in self.table: conn.execute(stmt) - def insert_statement(self): + def insert_statement(self, *, num_rows): names = list(map(str, self.frame.columns)) wld = "?" # wildcard char escape = _get_valid_sqlite_name @@ -1451,15 +1451,22 @@ def insert_statement(self): bracketed_names = [escape(column) for column in names] col_names = ",".join(bracketed_names) - wildcards = ",".join([wld] * len(names)) + + row_wildcards = ",".join([wld] * len(names)) + wildcards = ",".join(f"({row_wildcards})" for _ in range(num_rows)) insert_statement = ( - f"INSERT INTO {escape(self.name)} ({col_names}) VALUES ({wildcards})" + f"INSERT INTO {escape(self.name)} ({col_names}) VALUES {wildcards}" ) return insert_statement def _execute_insert(self, conn, keys, data_iter): data_list = list(data_iter) - conn.executemany(self.insert_statement(), data_list) + conn.executemany(self.insert_statement(num_rows=1), data_list) + + def _execute_insert_multi(self, conn, keys, data_iter): + data_list = list(data_iter) + flattened_data = [x for row in data_list for x in row] + conn.execute(self.insert_statement(num_rows=len(data_list)), flattened_data) def _create_table_setup(self): """ @@ -1599,7 +1606,6 @@ def _query_iterator( cursor, chunksize, columns, index_col=None, coerce_float=True, parse_dates=None ): """Return generator through chunked result set""" - while True: data = cursor.fetchmany(chunksize) if type(data) == tuple: @@ -1774,6 +1780,5 @@ def get_schema(frame, name, keys=None, con=None, dtype=None): be a SQLAlchemy type, or a string for sqlite3 fallback connection. """ - pandas_sql = pandasSQL_builder(con=con) return pandas_sql._create_sql_schema(frame, name, keys=keys, dtype=dtype) diff --git a/pandas/io/stata.py b/pandas/io/stata.py index 06bf906be7093..0397dfa923afb 100644 --- a/pandas/io/stata.py +++ b/pandas/io/stata.py @@ -482,7 +482,8 @@ class InvalidColumnName(Warning): def _cast_to_stata_types(data: DataFrame) -> DataFrame: - """Checks the dtypes of the columns of a pandas DataFrame for + """ + Checks the dtypes of the columns of a pandas DataFrame for compatibility with the data types and ranges supported by Stata, and converts if necessary. @@ -1160,8 +1161,8 @@ def f(typ: int) -> Union[int, str]: return typ try: return self.TYPE_MAP_XML[typ] - except KeyError: - raise ValueError(f"cannot convert stata types [{typ}]") + except KeyError as err: + raise ValueError(f"cannot convert stata types [{typ}]") from err typlist = [f(x) for x in raw_typlist] @@ -1170,8 +1171,8 @@ def g(typ: int) -> Union[str, np.dtype]: return str(typ) try: return self.DTYPE_MAP_XML[typ] - except KeyError: - raise ValueError(f"cannot convert stata dtype [{typ}]") + except KeyError as err: + raise ValueError(f"cannot convert stata dtype [{typ}]") from err dtyplist = [g(x) for x in raw_typlist] @@ -1295,14 +1296,14 @@ def _read_old_header(self, first_char: bytes) -> None: try: self.typlist = [self.TYPE_MAP[typ] for typ in typlist] - except ValueError: + except ValueError as err: invalid_types = ",".join(str(x) for x in typlist) - raise ValueError(f"cannot convert stata types [{invalid_types}]") + raise ValueError(f"cannot convert stata types [{invalid_types}]") from err try: self.dtyplist = [self.DTYPE_MAP[typ] for typ in typlist] - except ValueError: + except ValueError as err: invalid_dtypes = ",".join(str(x) for x in typlist) - raise ValueError(f"cannot convert stata dtypes [{invalid_dtypes}]") + raise ValueError(f"cannot convert stata dtypes [{invalid_dtypes}]") from err if self.format_version > 108: self.varlist = [ @@ -1671,7 +1672,7 @@ def _do_convert_missing(self, data: DataFrame, convert_missing: bool) -> DataFra continue if convert_missing: # Replacement follows Stata notation - missing_loc = np.argwhere(missing._ndarray_values) + missing_loc = np.nonzero(missing._ndarray_values)[0] umissing, umissing_loc = np.unique(series[missing], return_inverse=True) replacement = Series(series, dtype=np.object) for j, um in enumerate(umissing): @@ -1760,7 +1761,7 @@ def _do_convert_categoricals( categories.append(category) # Partially labeled try: cat_data.categories = categories - except ValueError: + except ValueError as err: vc = Series(categories).value_counts() repeated_cats = list(vc.index[vc > 1]) repeats = "-" * 80 + "\n" + "\n".join(repeated_cats) @@ -1776,7 +1777,7 @@ def _do_convert_categoricals( The repeated labels are: {repeats} """ - raise ValueError(msg) + raise ValueError(msg) from err # TODO: is the next line needed above in the data(...) method? cat_series = Series(cat_data, index=data.index) cat_converted_data.append((col, cat_series)) @@ -2128,9 +2129,10 @@ def _write_bytes(self, value: bytes) -> None: self._file.write(value) def _prepare_categoricals(self, data: DataFrame) -> DataFrame: - """Check for categorical columns, retain categorical information for - Stata file and convert categorical data to int""" - + """ + Check for categorical columns, retain categorical information for + Stata file and convert categorical data to int + """ is_cat = [is_categorical_dtype(data[col]) for col in data] self._is_col_cat = is_cat self._value_labels: List[StataValueLabel] = [] @@ -2170,8 +2172,10 @@ def _prepare_categoricals(self, data: DataFrame) -> DataFrame: def _replace_nans(self, data: DataFrame) -> DataFrame: # return data - """Checks floating point data columns for nans, and replaces these with - the generic Stata for missing value (.)""" + """ + Checks floating point data columns for nans, and replaces these with + the generic Stata for missing value (.) + """ for c in data: dtype = data[c].dtype if dtype in (np.float32, np.float64): @@ -2769,7 +2773,6 @@ def generate_table(self) -> Tuple[Dict[str, Tuple[int, int]], DataFrame]: * 118: 6 * 119: 5 """ - gso_table = self._gso_table gso_df = self.df columns = list(gso_df.columns) @@ -3035,9 +3038,11 @@ def _write_header( self._write_bytes(self._tag(bio.read(), "header")) def _write_map(self) -> None: - """Called twice during file write. The first populates the values in + """ + Called twice during file write. The first populates the values in the map with 0s. The second call writes the final map locations when - all blocks have been written.""" + all blocks have been written. + """ assert self._file is not None if not self._map: self._map = dict( @@ -3138,11 +3143,11 @@ def _write_variable_labels(self) -> None: raise ValueError("Variable labels must be 80 characters or fewer") try: encoded = label.encode(self._encoding) - except UnicodeEncodeError: + except UnicodeEncodeError as err: raise ValueError( "Variable labels must contain only characters that " f"can be encoded in {self._encoding}" - ) + ) from err bio.write(_pad_bytes_new(encoded, vl_len + 1)) else: @@ -3184,8 +3189,10 @@ def _write_file_close_tag(self) -> None: self._update_map("end-of-file") def _update_strl_names(self) -> None: - """Update column names for conversion to strl if they might have been - changed to comply with Stata naming rules""" + """ + Update column names for conversion to strl if they might have been + changed to comply with Stata naming rules + """ # Update convert_strl if names changed for orig, new in self._converted_names.items(): if orig in self._convert_strl: @@ -3193,8 +3200,10 @@ def _update_strl_names(self) -> None: self._convert_strl[idx] = new def _convert_strls(self, data: DataFrame) -> DataFrame: - """Convert columns to StrLs if either very large or in the - convert_strl variable""" + """ + Convert columns to StrLs if either very large or in the + convert_strl variable + """ convert_cols = [ col for i, col in enumerate(data) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 1fe383706f74d..d3db539084609 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -176,13 +176,12 @@ def hist_frame( Examples -------- + This example draws a histogram based on the length and width of + some animals, displayed in three bins .. plot:: :context: close-figs - This example draws a histogram based on the length and width of - some animals, displayed in three bins - >>> df = pd.DataFrame({ ... 'length': [1.5, 0.5, 1.2, 0.9, 3], ... 'width': [0.7, 0.2, 0.15, 0.2, 1.1] diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index deeeb0016142c..e36696dc23a87 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -107,10 +107,16 @@ def maybe_color_bp(self, bp): medians = self.color or self._medians_c caps = self.color or self._caps_c - setp(bp["boxes"], color=boxes, alpha=1) - setp(bp["whiskers"], color=whiskers, alpha=1) - setp(bp["medians"], color=medians, alpha=1) - setp(bp["caps"], color=caps, alpha=1) + # GH 30346, when users specifying those arguments explicitly, our defaults + # for these four kwargs should be overridden; if not, use Pandas settings + if not self.kwds.get("boxprops"): + setp(bp["boxes"], color=boxes, alpha=1) + if not self.kwds.get("whiskerprops"): + setp(bp["whiskers"], color=whiskers, alpha=1) + if not self.kwds.get("medianprops"): + setp(bp["medians"], color=medians, alpha=1) + if not self.kwds.get("capprops"): + setp(bp["caps"], color=caps, alpha=1) def _make_plot(self): if self.subplots: @@ -275,11 +281,17 @@ def _get_colors(): return result - def maybe_color_bp(bp): - setp(bp["boxes"], color=colors[0], alpha=1) - setp(bp["whiskers"], color=colors[1], alpha=1) - setp(bp["medians"], color=colors[2], alpha=1) - setp(bp["caps"], color=colors[3], alpha=1) + def maybe_color_bp(bp, **kwds): + # GH 30346, when users specifying those arguments explicitly, our defaults + # for these four kwargs should be overridden; if not, use Pandas settings + if not kwds.get("boxprops"): + setp(bp["boxes"], color=colors[0], alpha=1) + if not kwds.get("whiskerprops"): + setp(bp["whiskers"], color=colors[1], alpha=1) + if not kwds.get("medianprops"): + setp(bp["medians"], color=colors[2], alpha=1) + if not kwds.get("capprops"): + setp(bp["caps"], color=colors[3], alpha=1) def plot_group(keys, values, ax): keys = [pprint_thing(x) for x in keys] @@ -291,7 +303,7 @@ def plot_group(keys, values, ax): ax.set_xticklabels(keys, rotation=rot) else: ax.set_yticklabels(keys, rotation=rot) - maybe_color_bp(bp) + maybe_color_bp(bp, **kwds) # Return axes in multiplot case, maybe revisit later # 985 if return_type == "dict": diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index a1035fd0823bb..c399e5b9b7017 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -981,8 +981,7 @@ def __init__( self.finder = get_finder(freq) def _get_default_locs(self, vmin, vmax): - "Returns the default locations of ticks." - + """Returns the default locations of ticks.""" if self.plot_obj.date_axis_info is None: self.plot_obj.date_axis_info = self.finder(vmin, vmax, self.freq) @@ -993,7 +992,7 @@ def _get_default_locs(self, vmin, vmax): return np.compress(locator["maj"], locator["val"]) def __call__(self): - "Return the locations of the ticks." + """Return the locations of the ticks.""" # axis calls Locator.set_axis inside set_m_formatter vi = tuple(self.axis.get_view_interval()) @@ -1062,8 +1061,7 @@ def __init__(self, freq, minor_locator=False, dynamic_mode=True, plot_obj=None): self.finder = get_finder(freq) def _set_default_format(self, vmin, vmax): - "Returns the default ticks spacing." - + """Returns the default ticks spacing.""" if self.plot_obj.date_axis_info is None: self.plot_obj.date_axis_info = self.finder(vmin, vmax, self.freq) info = self.plot_obj.date_axis_info @@ -1076,7 +1074,7 @@ def _set_default_format(self, vmin, vmax): return self.formatdict def set_locs(self, locs): - "Sets the locations of the ticks" + """Sets the locations of the ticks""" # don't actually use the locs. This is just needed to work with # matplotlib. Force to use vmin, vmax diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index de09460bb833d..63d0b8abe59d9 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -432,7 +432,6 @@ def _add_table(self): def _post_plot_logic_common(self, ax, data): """Common post process for each axes""" - if self.orientation == "vertical" or self.orientation is None: self._apply_axis_properties(ax.xaxis, rot=self.rot, fontsize=self.fontsize) self._apply_axis_properties(ax.yaxis, fontsize=self.fontsize) @@ -509,12 +508,12 @@ def _adorn_subplots(self): self.axes[0].set_title(self.title) def _apply_axis_properties(self, axis, rot=None, fontsize=None): - """ Tick creation within matplotlib is reasonably expensive and is - internally deferred until accessed as Ticks are created/destroyed - multiple times per draw. It's therefore beneficial for us to avoid - accessing unless we will act on the Tick. """ - + Tick creation within matplotlib is reasonably expensive and is + internally deferred until accessed as Ticks are created/destroyed + multiple times per draw. It's therefore beneficial for us to avoid + accessing unless we will act on the Tick. + """ if rot is not None or fontsize is not None: # rot=0 is a valid setting, hence the explicit None check labels = axis.get_majorticklabels() + axis.get_minorticklabels() @@ -755,7 +754,6 @@ def _parse_errorbars(self, label, err): key in the plotted DataFrame str: the name of the column within the plotted DataFrame """ - if err is None: return None diff --git a/pandas/plotting/_matplotlib/tools.py b/pandas/plotting/_matplotlib/tools.py index d7732c86911b8..5743288982da4 100644 --- a/pandas/plotting/_matplotlib/tools.py +++ b/pandas/plotting/_matplotlib/tools.py @@ -100,13 +100,14 @@ def _subplots( layout_type="box", **fig_kw, ): - """Create a figure with a set of subplots already made. + """ + Create a figure with a set of subplots already made. This utility wrapper makes it convenient to create common layouts of subplots, including the enclosing figure object, in a single call. - Keyword arguments: - + Parameters + ---------- naxes : int Number of required axes. Exceeded axes are set invisible. Default is nrows * ncols. @@ -146,16 +147,16 @@ def _subplots( Note that all keywords not recognized above will be automatically included here. - Returns: - + Returns + ------- fig, ax : tuple - fig is the Matplotlib Figure object - ax can be either a single axis object or an array of axis objects if more than one subplot was created. The dimensions of the resulting array can be controlled with the squeeze keyword, see above. - **Examples:** - + Examples + -------- x = np.linspace(0, 2*np.pi, 400) y = np.sin(x**2) diff --git a/pandas/plotting/_misc.py b/pandas/plotting/_misc.py index 1369adcd80269..47a4fd8ff0e95 100644 --- a/pandas/plotting/_misc.py +++ b/pandas/plotting/_misc.py @@ -294,6 +294,7 @@ def bootstrap_plot(series, fig=None, size=50, samples=500, **kwds): Examples -------- + This example draws a basic bootstap plot for a Series. .. plot:: :context: close-figs diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 406d5f055797d..5aab5b814bae7 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -198,6 +198,7 @@ class TestPDApi(Base): "_np_version_under1p16", "_np_version_under1p17", "_np_version_under1p18", + "_is_numpy_dev", "_testing", "_tslib", "_typing", diff --git a/pandas/tests/arithmetic/common.py b/pandas/tests/arithmetic/common.py index 83d19b8a20ac3..ccc49adc5da82 100644 --- a/pandas/tests/arithmetic/common.py +++ b/pandas/tests/arithmetic/common.py @@ -13,7 +13,7 @@ def assert_invalid_addsub_type(left, right, msg=None): Helper to assert that left and right can be neither added nor subtracted. Parameters - --------- + ---------- left : object right : object msg : str or None, default None diff --git a/pandas/tests/arithmetic/test_interval.py b/pandas/tests/arithmetic/test_interval.py index f9e1a515277d5..3f85ac8c190db 100644 --- a/pandas/tests/arithmetic/test_interval.py +++ b/pandas/tests/arithmetic/test_interval.py @@ -129,6 +129,10 @@ def test_compare_scalar_interval_mixed_closed(self, op, closed, other_closed): def test_compare_scalar_na(self, op, array, nulls_fixture): result = op(array, nulls_fixture) expected = self.elementwise_comparison(op, array, nulls_fixture) + + if nulls_fixture is pd.NA and array.dtype != pd.IntervalDtype("int"): + pytest.xfail("broken for non-integer IntervalArray; see GH 31882") + tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize( @@ -207,6 +211,10 @@ def test_compare_list_like_nan(self, op, array, nulls_fixture): other = [nulls_fixture] * 4 result = op(array, other) expected = self.elementwise_comparison(op, array, other) + + if nulls_fixture is pd.NA: + pytest.xfail("broken for non-integer IntervalArray; see GH 31882") + tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize( diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 51d09a92773b1..d4baf2f374cdf 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -66,8 +66,7 @@ def test_df_numeric_cmp_dt64_raises(self): ts = pd.Timestamp.now() df = pd.DataFrame({"x": range(5)}) - msg = "Invalid comparison between dtype=int64 and Timestamp" - + msg = "'[<>]' not supported between instances of 'Timestamp' and 'int'" with pytest.raises(TypeError, match=msg): df > ts with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index abb667260f094..4cf1988a33de1 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -153,14 +153,17 @@ def test_eq_integer_disallowed(self, other): result = idx == other tm.assert_numpy_array_equal(result, expected) - - with pytest.raises(TypeError): + msg = ( + r"(:?Invalid comparison between dtype=period\[D\] and .*)" + r"|(:?Cannot compare type Period with type .*)" + ) + with pytest.raises(TypeError, match=msg): idx < other - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): idx > other - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): idx <= other - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): idx >= other def test_pi_cmp_period(self): @@ -587,10 +590,11 @@ def test_parr_add_iadd_parr_raises(self, box_with_array): # a set operation (union). This has since been changed to # raise a TypeError. See GH#14164 and GH#13077 for historical # reference. - with pytest.raises(TypeError): + msg = r"unsupported operand type\(s\) for \+: .* and .*" + with pytest.raises(TypeError, match=msg): rng + other - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): rng += other def test_pi_sub_isub_pi(self): @@ -625,7 +629,8 @@ def test_parr_sub_pi_mismatched_freq(self, box_with_array): # TODO: parametrize over boxes for other? rng = tm.box_expected(rng, box_with_array) - with pytest.raises(IncompatibleFrequency): + msg = r"Input has different freq=[HD] from PeriodArray\(freq=[DH]\)" + with pytest.raises(IncompatibleFrequency, match=msg): rng - other @pytest.mark.parametrize("n", [1, 2, 3, 4]) @@ -677,7 +682,8 @@ def test_parr_add_sub_float_raises(self, op, other, box_with_array): dti = pd.DatetimeIndex(["2011-01-01", "2011-01-02"], freq="D") pi = dti.to_period("D") pi = tm.box_expected(pi, box_with_array) - with pytest.raises(TypeError): + msg = r"unsupported operand type\(s\) for [+-]: .* and .*" + with pytest.raises(TypeError, match=msg): op(pi, other) @pytest.mark.parametrize( @@ -700,13 +706,18 @@ def test_parr_add_sub_invalid(self, other, box_with_array): rng = pd.period_range("1/1/2000", freq="D", periods=3) rng = tm.box_expected(rng, box_with_array) - with pytest.raises(TypeError): + msg = ( + r"(:?cannot add PeriodArray and .*)" + r"|(:?cannot subtract .* from (:?a\s)?.*)" + r"|(:?unsupported operand type\(s\) for \+: .* and .*)" + ) + with pytest.raises(TypeError, match=msg): rng + other - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): other + rng - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): rng - other - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): other - rng # ----------------------------------------------------------------- @@ -717,14 +728,16 @@ def test_pi_add_sub_td64_array_non_tick_raises(self): tdi = pd.TimedeltaIndex(["-1 Day", "-1 Day", "-1 Day"]) tdarr = tdi.values - with pytest.raises(IncompatibleFrequency): + msg = r"Input has different freq=None from PeriodArray\(freq=Q-DEC\)" + with pytest.raises(IncompatibleFrequency, match=msg): rng + tdarr - with pytest.raises(IncompatibleFrequency): + with pytest.raises(IncompatibleFrequency, match=msg): tdarr + rng - with pytest.raises(IncompatibleFrequency): + with pytest.raises(IncompatibleFrequency, match=msg): rng - tdarr - with pytest.raises(TypeError): + msg = r"cannot subtract PeriodArray from timedelta64\[ns\]" + with pytest.raises(TypeError, match=msg): tdarr - rng def test_pi_add_sub_td64_array_tick(self): @@ -751,10 +764,11 @@ def test_pi_add_sub_td64_array_tick(self): result = rng - tdarr tm.assert_index_equal(result, expected) - with pytest.raises(TypeError): + msg = r"cannot subtract .* from .*" + with pytest.raises(TypeError, match=msg): tdarr - rng - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): tdi - rng # ----------------------------------------------------------------- @@ -783,10 +797,11 @@ def test_pi_add_offset_array(self, box): unanchored = np.array([pd.offsets.Hour(n=1), pd.offsets.Minute(n=-2)]) # addition/subtraction ops with incompatible offsets should issue # a PerformanceWarning and _then_ raise a TypeError. - with pytest.raises(IncompatibleFrequency): + msg = r"Input cannot be converted to Period\(freq=Q-DEC\)" + with pytest.raises(IncompatibleFrequency, match=msg): with tm.assert_produces_warning(PerformanceWarning): pi + unanchored - with pytest.raises(IncompatibleFrequency): + with pytest.raises(IncompatibleFrequency, match=msg): with tm.assert_produces_warning(PerformanceWarning): unanchored + pi @@ -811,10 +826,11 @@ def test_pi_sub_offset_array(self, box): # addition/subtraction ops with anchored offsets should issue # a PerformanceWarning and _then_ raise a TypeError. - with pytest.raises(IncompatibleFrequency): + msg = r"Input has different freq=-1M from Period\(freq=Q-DEC\)" + with pytest.raises(IncompatibleFrequency, match=msg): with tm.assert_produces_warning(PerformanceWarning): pi - anchored - with pytest.raises(IncompatibleFrequency): + with pytest.raises(IncompatibleFrequency, match=msg): with tm.assert_produces_warning(PerformanceWarning): anchored - pi @@ -924,7 +940,8 @@ def test_pi_sub_intarray(self, int_holder): expected = pd.PeriodIndex([pd.Period("2014Q1"), pd.Period("NaT")]) tm.assert_index_equal(result, expected) - with pytest.raises(TypeError): + msg = r"bad operand type for unary -: 'PeriodArray'" + with pytest.raises(TypeError, match=msg): other - pi # --------------------------------------------------------------- @@ -952,7 +969,11 @@ def test_pi_add_timedeltalike_minute_gt1(self, three_days): result = rng - other tm.assert_index_equal(result, expected) - with pytest.raises(TypeError): + msg = ( + r"(:?bad operand type for unary -: 'PeriodArray')" + r"|(:?cannot subtract PeriodArray from timedelta64\[[hD]\])" + ) + with pytest.raises(TypeError, match=msg): other - rng @pytest.mark.parametrize("freqstr", ["5ns", "5us", "5ms", "5s", "5T", "5h", "5d"]) @@ -974,8 +995,11 @@ def test_pi_add_timedeltalike_tick_gt1(self, three_days, freqstr): expected = pd.period_range(rng[0] - other, periods=6, freq=freqstr) result = rng - other tm.assert_index_equal(result, expected) - - with pytest.raises(TypeError): + msg = ( + r"(:?bad operand type for unary -: 'PeriodArray')" + r"|(:?cannot subtract PeriodArray from timedelta64\[[hD]\])" + ) + with pytest.raises(TypeError, match=msg): other - rng def test_pi_add_iadd_timedeltalike_daily(self, three_days): @@ -1110,7 +1134,8 @@ def test_parr_add_sub_td64_nat(self, box_with_array, transpose): tm.assert_equal(result, expected) result = obj - other tm.assert_equal(result, expected) - with pytest.raises(TypeError): + msg = r"cannot subtract .* from .*" + with pytest.raises(TypeError, match=msg): other - obj @pytest.mark.parametrize( @@ -1133,7 +1158,8 @@ def test_parr_add_sub_tdt64_nat_array(self, box_with_array, other): tm.assert_equal(result, expected) result = obj - other tm.assert_equal(result, expected) - with pytest.raises(TypeError): + msg = r"cannot subtract .* from .*" + with pytest.raises(TypeError, match=msg): other - obj # --------------------------------------------------------------- diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index abdeb1b30b626..300e468c34e65 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -25,6 +25,19 @@ get_upcast_box, ) + +def assert_dtype(obj, expected_dtype): + """ + Helper to check the dtype for a Series, Index, or single-column DataFrame. + """ + if isinstance(obj, DataFrame): + dtype = obj.dtypes.iat[0] + else: + dtype = obj.dtype + + assert dtype == expected_dtype + + # ------------------------------------------------------------------ # Timedelta64[ns] dtype Comparisons @@ -522,19 +535,35 @@ def test_tda_add_sub_index(self): # ------------------------------------------------------------- # Binary operations TimedeltaIndex and timedelta-like - def test_tdi_iadd_timedeltalike(self, two_hours): + def test_tdi_iadd_timedeltalike(self, two_hours, box_with_array): # only test adding/sub offsets as + is now numeric rng = timedelta_range("1 days", "10 days") expected = timedelta_range("1 days 02:00:00", "10 days 02:00:00", freq="D") + + rng = tm.box_expected(rng, box_with_array) + expected = tm.box_expected(expected, box_with_array) + + orig_rng = rng rng += two_hours - tm.assert_index_equal(rng, expected) + tm.assert_equal(rng, expected) + if box_with_array is not pd.Index: + # Check that operation is actually inplace + tm.assert_equal(orig_rng, expected) - def test_tdi_isub_timedeltalike(self, two_hours): + def test_tdi_isub_timedeltalike(self, two_hours, box_with_array): # only test adding/sub offsets as - is now numeric rng = timedelta_range("1 days", "10 days") expected = timedelta_range("0 days 22:00:00", "9 days 22:00:00") + + rng = tm.box_expected(rng, box_with_array) + expected = tm.box_expected(expected, box_with_array) + + orig_rng = rng rng -= two_hours - tm.assert_index_equal(rng, expected) + tm.assert_equal(rng, expected) + if box_with_array is not pd.Index: + # Check that operation is actually inplace + tm.assert_equal(orig_rng, expected) # ------------------------------------------------------------- @@ -1013,15 +1042,6 @@ def test_td64arr_add_datetime64_nat(self, box_with_array): # ------------------------------------------------------------------ # Invalid __add__/__sub__ operations - # TODO: moved from frame tests; needs parametrization/de-duplication - 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` - assert_invalid_addsub_type(df, other) - @pytest.mark.parametrize("pi_freq", ["D", "W", "Q", "H"]) @pytest.mark.parametrize("tdi_freq", [None, "H"]) def test_td64arr_sub_periodlike(self, box_with_array, tdi_freq, pi_freq): @@ -1100,6 +1120,9 @@ def test_td64arr_add_sub_int(self, box_with_array, one): def test_td64arr_add_sub_integer_array(self, box_with_array): # GH#19959, deprecated GH#22535 + # GH#22696 for DataFrame case, check that we don't dispatch to numpy + # implementation, which treats int64 as m8[ns] + rng = timedelta_range("1 days 09:00:00", freq="H", periods=3) tdarr = tm.box_expected(rng, box_with_array) other = tm.box_expected([4, 3, 2], box_with_array) @@ -1119,60 +1142,6 @@ def test_td64arr_addsub_integer_array_no_freq(self, box_with_array): # ------------------------------------------------------------------ # Operations with timedelta-like others - # TODO: this was taken from tests.series.test_ops; de-duplicate - def test_operators_timedelta64_with_timedelta(self, scalar_td): - # smoke tests - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - - td1 + scalar_td - scalar_td + td1 - td1 - scalar_td - scalar_td - td1 - td1 / scalar_td - scalar_td / td1 - - # TODO: this was taken from tests.series.test_ops; de-duplicate - def test_timedelta64_operations_with_timedeltas(self): - # td operate with td - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td2 = timedelta(minutes=5, seconds=4) - result = td1 - td2 - expected = Series([timedelta(seconds=0)] * 3) - Series( - [timedelta(seconds=1)] * 3 - ) - assert result.dtype == "m8[ns]" - tm.assert_series_equal(result, expected) - - result2 = td2 - td1 - expected = Series([timedelta(seconds=1)] * 3) - Series( - [timedelta(seconds=0)] * 3 - ) - tm.assert_series_equal(result2, expected) - - # roundtrip - tm.assert_series_equal(result + td2, td1) - - # Now again, using pd.to_timedelta, which should build - # a Series or a scalar, depending on input. - td1 = Series(pd.to_timedelta(["00:05:03"] * 3)) - td2 = pd.to_timedelta("00:05:04") - result = td1 - td2 - expected = Series([timedelta(seconds=0)] * 3) - Series( - [timedelta(seconds=1)] * 3 - ) - assert result.dtype == "m8[ns]" - tm.assert_series_equal(result, expected) - - result2 = td2 - td1 - expected = Series([timedelta(seconds=1)] * 3) - Series( - [timedelta(seconds=0)] * 3 - ) - tm.assert_series_equal(result2, expected) - - # roundtrip - tm.assert_series_equal(result + td2, td1) - def test_td64arr_add_td64_array(self, box_with_array): box = box_with_array dti = pd.date_range("2016-01-01", periods=3) @@ -1203,7 +1172,6 @@ def test_td64arr_sub_td64_array(self, box_with_array): result = tdarr - tdi tm.assert_equal(result, expected) - # TODO: parametrize over [add, sub, radd, rsub]? @pytest.mark.parametrize( "names", [ @@ -1232,17 +1200,11 @@ def test_td64arr_add_sub_tdi(self, box, names): result = tdi + ser tm.assert_equal(result, expected) - if box is not pd.DataFrame: - assert result.dtype == "timedelta64[ns]" - else: - assert result.dtypes[0] == "timedelta64[ns]" + assert_dtype(result, "timedelta64[ns]") result = ser + tdi tm.assert_equal(result, expected) - if box is not pd.DataFrame: - assert result.dtype == "timedelta64[ns]" - else: - assert result.dtypes[0] == "timedelta64[ns]" + assert_dtype(result, "timedelta64[ns]") expected = Series( [Timedelta(hours=-3), Timedelta(days=1, hours=-4)], name=names[2] @@ -1251,17 +1213,11 @@ def test_td64arr_add_sub_tdi(self, box, names): result = tdi - ser tm.assert_equal(result, expected) - if box is not pd.DataFrame: - assert result.dtype == "timedelta64[ns]" - else: - assert result.dtypes[0] == "timedelta64[ns]" + assert_dtype(result, "timedelta64[ns]") result = ser - tdi tm.assert_equal(result, -expected) - if box is not pd.DataFrame: - assert result.dtype == "timedelta64[ns]" - else: - assert result.dtypes[0] == "timedelta64[ns]" + assert_dtype(result, "timedelta64[ns]") def test_td64arr_add_sub_td64_nat(self, box_with_array): # GH#23320 special handling for timedelta64("NaT") @@ -1296,6 +1252,7 @@ def test_td64arr_sub_NaT(self, box_with_array): def test_td64arr_add_timedeltalike(self, two_hours, box_with_array): # only test adding/sub offsets as + is now numeric + # GH#10699 for Tick cases box = box_with_array rng = timedelta_range("1 days", "10 days") expected = timedelta_range("1 days 02:00:00", "10 days 02:00:00", freq="D") @@ -1305,8 +1262,12 @@ def test_td64arr_add_timedeltalike(self, two_hours, box_with_array): result = rng + two_hours tm.assert_equal(result, expected) + result = two_hours + rng + tm.assert_equal(result, expected) + def test_td64arr_sub_timedeltalike(self, two_hours, box_with_array): # only test adding/sub offsets as - is now numeric + # GH#10699 for Tick cases box = box_with_array rng = timedelta_range("1 days", "10 days") expected = timedelta_range("0 days 22:00:00", "9 days 22:00:00") @@ -1317,46 +1278,12 @@ def test_td64arr_sub_timedeltalike(self, two_hours, box_with_array): result = rng - two_hours tm.assert_equal(result, expected) + result = two_hours - rng + tm.assert_equal(result, -expected) + # ------------------------------------------------------------------ # __add__/__sub__ with DateOffsets and arrays of DateOffsets - # TODO: this was taken from tests.series.test_operators; de-duplicate - def test_timedelta64_operations_with_DateOffset(self): - # GH#10699 - td = Series([timedelta(minutes=5, seconds=3)] * 3) - result = td + pd.offsets.Minute(1) - expected = Series([timedelta(minutes=6, seconds=3)] * 3) - tm.assert_series_equal(result, expected) - - result = td - pd.offsets.Minute(1) - expected = Series([timedelta(minutes=4, seconds=3)] * 3) - tm.assert_series_equal(result, expected) - - with tm.assert_produces_warning(PerformanceWarning): - result = td + Series( - [pd.offsets.Minute(1), pd.offsets.Second(3), pd.offsets.Hour(2)] - ) - expected = Series( - [ - timedelta(minutes=6, seconds=3), - timedelta(minutes=5, seconds=6), - timedelta(hours=2, minutes=5, seconds=3), - ] - ) - tm.assert_series_equal(result, expected) - - result = td + pd.offsets.Minute(1) + pd.offsets.Second(12) - expected = Series([timedelta(minutes=6, seconds=15)] * 3) - tm.assert_series_equal(result, expected) - - # valid DateOffsets - for do in ["Hour", "Minute", "Second", "Day", "Micro", "Milli", "Nano"]: - op = getattr(pd.offsets, do) - td + op(5) - op(5) + td - td - op(5) - op(5) - td - @pytest.mark.parametrize( "names", [(None, None, None), ("foo", "bar", None), ("foo", "foo", "foo")] ) @@ -1561,26 +1488,6 @@ class TestTimedeltaArraylikeMulDivOps: # Tests for timedelta64[ns] # __mul__, __rmul__, __div__, __rdiv__, __floordiv__, __rfloordiv__ - # TODO: Moved from tests.series.test_operators; needs cleanup - @pytest.mark.parametrize("m", [1, 3, 10]) - @pytest.mark.parametrize("unit", ["D", "h", "m", "s", "ms", "us", "ns"]) - def test_timedelta64_conversions(self, m, unit): - startdate = Series(pd.date_range("2013-01-01", "2013-01-03")) - enddate = Series(pd.date_range("2013-03-01", "2013-03-03")) - - ser = enddate - startdate - ser[2] = np.nan - - # op - expected = Series([x / np.timedelta64(m, unit) for x in ser]) - result = ser / np.timedelta64(m, unit) - tm.assert_series_equal(result, expected) - - # reverse op - expected = Series([Timedelta(np.timedelta64(m, unit)) / x for x in ser]) - result = np.timedelta64(m, unit) / ser - tm.assert_series_equal(result, expected) - # ------------------------------------------------------------------ # Multiplication # organized with scalar others first, then array-like @@ -1734,6 +1641,29 @@ def test_td64arr_div_tdlike_scalar(self, two_hours, box_with_array): expected = 1 / expected tm.assert_equal(result, expected) + @pytest.mark.parametrize("m", [1, 3, 10]) + @pytest.mark.parametrize("unit", ["D", "h", "m", "s", "ms", "us", "ns"]) + def test_td64arr_div_td64_scalar(self, m, unit, box_with_array): + startdate = Series(pd.date_range("2013-01-01", "2013-01-03")) + enddate = Series(pd.date_range("2013-03-01", "2013-03-03")) + + ser = enddate - startdate + ser[2] = np.nan + flat = ser + ser = tm.box_expected(ser, box_with_array) + + # op + expected = Series([x / np.timedelta64(m, unit) for x in flat]) + expected = tm.box_expected(expected, box_with_array) + result = ser / np.timedelta64(m, unit) + tm.assert_equal(result, expected) + + # reverse op + expected = Series([Timedelta(np.timedelta64(m, unit)) / x for x in flat]) + expected = tm.box_expected(expected, box_with_array) + result = np.timedelta64(m, unit) / ser + tm.assert_equal(result, expected) + def test_td64arr_div_tdlike_scalar_with_nat(self, two_hours, box_with_array): rng = TimedeltaIndex(["1 days", pd.NaT, "2 days"], name="foo") expected = pd.Float64Index([12, np.nan, 24], name="foo") diff --git a/pandas/tests/arrays/categorical/test_analytics.py b/pandas/tests/arrays/categorical/test_analytics.py index 90fcf12093909..0ff7d3e59abb3 100644 --- a/pandas/tests/arrays/categorical/test_analytics.py +++ b/pandas/tests/arrays/categorical/test_analytics.py @@ -15,10 +15,10 @@ class TestCategoricalAnalytics: def test_min_max_not_ordered_raises(self, aggregation): # unordered cats have no min/max cat = Categorical(["a", "b", "c", "d"], ordered=False) - msg = "Categorical is not ordered for operation {}" + msg = f"Categorical is not ordered for operation {aggregation}" agg_func = getattr(cat, aggregation) - with pytest.raises(TypeError, match=msg.format(aggregation)): + with pytest.raises(TypeError, match=msg): agg_func() def test_min_max_ordered(self): diff --git a/pandas/tests/arrays/categorical/test_constructors.py b/pandas/tests/arrays/categorical/test_constructors.py index 70e1421c8dcf4..c6b4c4904735c 100644 --- a/pandas/tests/arrays/categorical/test_constructors.py +++ b/pandas/tests/arrays/categorical/test_constructors.py @@ -353,9 +353,9 @@ def test_constructor_from_index_series_period(self): result = Categorical(Series(idx)) tm.assert_index_equal(result.categories, idx) - def test_constructor_invariant(self): - # GH 14190 - vals = [ + @pytest.mark.parametrize( + "values", + [ np.array([1.0, 1.2, 1.8, np.nan]), np.array([1, 2, 3], dtype="int64"), ["a", "b", "c", np.nan], @@ -366,11 +366,13 @@ def test_constructor_invariant(self): Timestamp("2014-01-02", tz="US/Eastern"), NaT, ], - ] - for val in vals: - c = Categorical(val) - c2 = Categorical(c) - tm.assert_categorical_equal(c, c2) + ], + ) + def test_constructor_invariant(self, values): + # GH 14190 + c = Categorical(values) + c2 = Categorical(c) + tm.assert_categorical_equal(c, c2) @pytest.mark.parametrize("ordered", [True, False]) def test_constructor_with_dtype(self, ordered): @@ -458,9 +460,26 @@ def test_constructor_with_categorical_categories(self): result = Categorical(["a", "b"], categories=CategoricalIndex(["a", "b", "c"])) tm.assert_categorical_equal(result, expected) - def test_from_codes(self): + @pytest.mark.parametrize("klass", [lambda x: np.array(x, dtype=object), list]) + def test_construction_with_null(self, klass, nulls_fixture): + # https://github.com/pandas-dev/pandas/issues/31927 + values = klass(["a", nulls_fixture, "b"]) + result = Categorical(values) + + dtype = CategoricalDtype(["a", "b"]) + codes = [0, -1, 1] + expected = Categorical.from_codes(codes=codes, dtype=dtype) + + tm.assert_categorical_equal(result, expected) + + def test_from_codes_empty(self): + cat = ["a", "b", "c"] + result = Categorical.from_codes([], categories=cat) + expected = Categorical([], categories=cat) - # too few categories + tm.assert_categorical_equal(result, expected) + + def test_from_codes_too_few_categories(self): dtype = CategoricalDtype(categories=[1, 2]) msg = "codes need to be between " with pytest.raises(ValueError, match=msg): @@ -468,22 +487,23 @@ def test_from_codes(self): with pytest.raises(ValueError, match=msg): Categorical.from_codes([1, 2], dtype=dtype) - # no int codes + def test_from_codes_non_int_codes(self): + dtype = CategoricalDtype(categories=[1, 2]) msg = "codes need to be array-like integers" with pytest.raises(ValueError, match=msg): Categorical.from_codes(["a"], categories=dtype.categories) with pytest.raises(ValueError, match=msg): Categorical.from_codes(["a"], dtype=dtype) - # no unique categories + def test_from_codes_non_unique_categories(self): with pytest.raises(ValueError, match="Categorical categories must be unique"): Categorical.from_codes([0, 1, 2], categories=["a", "a", "b"]) - # NaN categories included + def test_from_codes_nan_cat_included(self): with pytest.raises(ValueError, match="Categorial categories cannot be null"): Categorical.from_codes([0, 1, 2], categories=["a", "b", np.nan]) - # too negative + def test_from_codes_too_negative(self): dtype = CategoricalDtype(categories=["a", "b", "c"]) msg = r"codes need to be between -1 and len\(categories\)-1" with pytest.raises(ValueError, match=msg): @@ -491,6 +511,8 @@ def test_from_codes(self): with pytest.raises(ValueError, match=msg): Categorical.from_codes([-2, 1, 2], dtype=dtype) + def test_from_codes(self): + dtype = CategoricalDtype(categories=["a", "b", "c"]) exp = Categorical(["a", "b", "c"], ordered=False) res = Categorical.from_codes([0, 1, 2], categories=dtype.categories) tm.assert_categorical_equal(exp, res) @@ -498,21 +520,18 @@ def test_from_codes(self): res = Categorical.from_codes([0, 1, 2], dtype=dtype) tm.assert_categorical_equal(exp, res) - def test_from_codes_with_categorical_categories(self): + @pytest.mark.parametrize("klass", [Categorical, CategoricalIndex]) + def test_from_codes_with_categorical_categories(self, klass): # GH17884 expected = Categorical(["a", "b"], categories=["a", "b", "c"]) - result = Categorical.from_codes([0, 1], categories=Categorical(["a", "b", "c"])) - tm.assert_categorical_equal(result, expected) - - result = Categorical.from_codes( - [0, 1], categories=CategoricalIndex(["a", "b", "c"]) - ) + result = Categorical.from_codes([0, 1], categories=klass(["a", "b", "c"])) tm.assert_categorical_equal(result, expected) - # non-unique Categorical still raises + @pytest.mark.parametrize("klass", [Categorical, CategoricalIndex]) + def test_from_codes_with_non_unique_categorical_categories(self, klass): with pytest.raises(ValueError, match="Categorical categories must be unique"): - Categorical.from_codes([0, 1], Categorical(["a", "b", "a"])) + Categorical.from_codes([0, 1], klass(["a", "b", "a"])) def test_from_codes_with_nan_code(self): # GH21767 @@ -523,24 +542,16 @@ def test_from_codes_with_nan_code(self): with pytest.raises(ValueError, match="codes need to be array-like integers"): Categorical.from_codes(codes, dtype=dtype) - def test_from_codes_with_float(self): + @pytest.mark.parametrize("codes", [[1.0, 2.0, 0], [1.1, 2.0, 0]]) + def test_from_codes_with_float(self, codes): # GH21767 - codes = [1.0, 2.0, 0] # integer, but in float dtype + # float codes should raise even if values are equal to integers dtype = CategoricalDtype(categories=["a", "b", "c"]) - # empty codes should not raise for floats - Categorical.from_codes([], dtype.categories) - - with pytest.raises(ValueError, match="codes need to be array-like integers"): - Categorical.from_codes(codes, dtype.categories) - - with pytest.raises(ValueError, match="codes need to be array-like integers"): - Categorical.from_codes(codes, dtype=dtype) - - codes = [1.1, 2.0, 0] # non-integer - with pytest.raises(ValueError, match="codes need to be array-like integers"): + msg = "codes need to be array-like integers" + with pytest.raises(ValueError, match=msg): Categorical.from_codes(codes, dtype.categories) - with pytest.raises(ValueError, match="codes need to be array-like integers"): + with pytest.raises(ValueError, match=msg): Categorical.from_codes(codes, dtype=dtype) def test_from_codes_with_dtype_raises(self): @@ -560,6 +571,23 @@ def test_from_codes_neither(self): with pytest.raises(ValueError, match=msg): Categorical.from_codes([0, 1]) + def test_from_codes_with_nullable_int(self): + codes = pd.array([0, 1], dtype="Int64") + categories = ["a", "b"] + + result = Categorical.from_codes(codes, categories=categories) + expected = Categorical.from_codes(codes.to_numpy(int), categories=categories) + + tm.assert_categorical_equal(result, expected) + + def test_from_codes_with_nullable_int_na_raises(self): + codes = pd.array([0, None], dtype="Int64") + categories = ["a", "b"] + + msg = "codes cannot contain NA values" + with pytest.raises(ValueError, match=msg): + Categorical.from_codes(codes, categories=categories) + @pytest.mark.parametrize("dtype", [None, "category"]) def test_from_inferred_categories(self, dtype): cats = ["a", "b"] diff --git a/pandas/tests/arrays/categorical/test_dtypes.py b/pandas/tests/arrays/categorical/test_dtypes.py index 19746d7d72162..9922a8863ebc2 100644 --- a/pandas/tests/arrays/categorical/test_dtypes.py +++ b/pandas/tests/arrays/categorical/test_dtypes.py @@ -92,22 +92,20 @@ def test_codes_dtypes(self): result = Categorical(["foo", "bar", "baz"]) assert result.codes.dtype == "int8" - result = Categorical(["foo{i:05d}".format(i=i) for i in range(400)]) + result = Categorical([f"foo{i:05d}" for i in range(400)]) assert result.codes.dtype == "int16" - result = Categorical(["foo{i:05d}".format(i=i) for i in range(40000)]) + result = Categorical([f"foo{i:05d}" for i in range(40000)]) assert result.codes.dtype == "int32" # adding cats result = Categorical(["foo", "bar", "baz"]) assert result.codes.dtype == "int8" - result = result.add_categories(["foo{i:05d}".format(i=i) for i in range(400)]) + result = result.add_categories([f"foo{i:05d}" for i in range(400)]) assert result.codes.dtype == "int16" # removing cats - result = result.remove_categories( - ["foo{i:05d}".format(i=i) for i in range(300)] - ) + result = result.remove_categories([f"foo{i:05d}" for i in range(300)]) assert result.codes.dtype == "int8" @pytest.mark.parametrize("ordered", [True, False]) diff --git a/pandas/tests/arrays/categorical/test_indexing.py b/pandas/tests/arrays/categorical/test_indexing.py index 85d5a6a3dc3ac..3d9469c252914 100644 --- a/pandas/tests/arrays/categorical/test_indexing.py +++ b/pandas/tests/arrays/categorical/test_indexing.py @@ -240,14 +240,17 @@ def test_mask_with_boolean(index): @pytest.mark.parametrize("index", [True, False]) -def test_mask_with_boolean_raises(index): +def test_mask_with_boolean_na_treated_as_false(index): + # https://github.com/pandas-dev/pandas/issues/31503 s = Series(range(3)) idx = Categorical([True, False, None]) if index: idx = CategoricalIndex(idx) - with pytest.raises(ValueError, match="NA / NaN"): - s[idx] + result = s[idx] + expected = s[idx.fillna(False)] + + tm.assert_series_equal(result, expected) @pytest.fixture diff --git a/pandas/tests/arrays/categorical/test_operators.py b/pandas/tests/arrays/categorical/test_operators.py index 0c830c65e0f8b..6ea003c122eea 100644 --- a/pandas/tests/arrays/categorical/test_operators.py +++ b/pandas/tests/arrays/categorical/test_operators.py @@ -338,7 +338,7 @@ def test_compare_unordered_different_order(self): def test_numeric_like_ops(self): df = DataFrame({"value": np.random.randint(0, 10000, 100)}) - labels = ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] + labels = [f"{i} - {i + 499}" for i in range(0, 10000, 500)] cat_labels = Categorical(labels, labels) df = df.sort_values(by=["value"], ascending=True) @@ -353,9 +353,7 @@ def test_numeric_like_ops(self): ("__mul__", r"\*"), ("__truediv__", "/"), ]: - msg = r"Series cannot perform the operation {}|unsupported operand".format( - str_rep - ) + msg = f"Series cannot perform the operation {str_rep}|unsupported operand" with pytest.raises(TypeError, match=msg): getattr(df, op)(df) @@ -363,7 +361,7 @@ def test_numeric_like_ops(self): # min/max) s = df["value_group"] for op in ["kurt", "skew", "var", "std", "mean", "sum", "median"]: - msg = "Categorical cannot perform the operation {}".format(op) + msg = f"Categorical cannot perform the operation {op}" with pytest.raises(TypeError, match=msg): getattr(s, op)(numeric_only=False) @@ -383,9 +381,7 @@ def test_numeric_like_ops(self): ("__mul__", r"\*"), ("__truediv__", "/"), ]: - msg = r"Series cannot perform the operation {}|unsupported operand".format( - str_rep - ) + msg = f"Series cannot perform the operation {str_rep}|unsupported operand" with pytest.raises(TypeError, match=msg): getattr(s, op)(2) diff --git a/pandas/tests/arrays/categorical/test_replace.py b/pandas/tests/arrays/categorical/test_replace.py new file mode 100644 index 0000000000000..52530123bd52f --- /dev/null +++ b/pandas/tests/arrays/categorical/test_replace.py @@ -0,0 +1,48 @@ +import pytest + +import pandas as pd +import pandas._testing as tm + + +@pytest.mark.parametrize( + "to_replace,value,expected,check_types,check_categorical", + [ + # one-to-one + (1, 2, [2, 2, 3], True, True), + (1, 4, [4, 2, 3], True, True), + (4, 1, [1, 2, 3], True, True), + (5, 6, [1, 2, 3], True, True), + # many-to-one + ([1], 2, [2, 2, 3], True, True), + ([1, 2], 3, [3, 3, 3], True, True), + ([1, 2], 4, [4, 4, 3], True, True), + ((1, 2, 4), 5, [5, 5, 3], True, True), + ((5, 6), 2, [1, 2, 3], True, True), + # many-to-many, handled outside of Categorical and results in separate dtype + ([1], [2], [2, 2, 3], False, False), + ([1, 4], [5, 2], [5, 2, 3], False, False), + # check_categorical sorts categories, which crashes on mixed dtypes + (3, "4", [1, 2, "4"], True, False), + ([1, 2, "3"], "5", ["5", "5", 3], True, False), + ], +) +def test_replace(to_replace, value, expected, check_types, check_categorical): + # GH 31720 + s = pd.Series([1, 2, 3], dtype="category") + result = s.replace(to_replace, value) + expected = pd.Series(expected, dtype="category") + s.replace(to_replace, value, inplace=True) + tm.assert_series_equal( + expected, + result, + check_dtype=check_types, + check_categorical=check_categorical, + check_category_order=False, + ) + tm.assert_series_equal( + expected, + s, + check_dtype=check_types, + check_categorical=check_categorical, + check_category_order=False, + ) diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index b1b5a9482e34f..f42b16cf18f20 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -291,7 +291,7 @@ class DecimalArray2(DecimalArray): @classmethod def _from_sequence(cls, scalars, dtype=None, copy=False): if isinstance(scalars, (pd.Series, pd.Index)): - raise TypeError + raise TypeError("scalars should not be of type pd.Series or pd.Index") return super()._from_sequence(scalars, dtype=dtype, copy=copy) @@ -301,7 +301,9 @@ def test_array_unboxes(index_or_series): data = box([decimal.Decimal("1"), decimal.Decimal("2")]) # make sure it works - with pytest.raises(TypeError): + with pytest.raises( + TypeError, match="scalars should not be of type pd.Series or pd.Index" + ): DecimalArray2._from_sequence(data) result = pd.array(data, dtype="decimal2") diff --git a/pandas/tests/arrays/test_boolean.py b/pandas/tests/arrays/test_boolean.py index cb9b07db4a0df..d14d6f3ff0c41 100644 --- a/pandas/tests/arrays/test_boolean.py +++ b/pandas/tests/arrays/test_boolean.py @@ -131,7 +131,8 @@ def test_to_boolean_array_missing_indicators(a, b): ) def test_to_boolean_array_error(values): # error in converting existing arrays to BooleanArray - with pytest.raises(TypeError): + msg = "Need to pass bool-like value" + with pytest.raises(TypeError, match=msg): pd.array(values, dtype="boolean") diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 87b825c8c27bd..17818b6ce689f 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -463,7 +463,7 @@ def test_concat_same_type_invalid(self, datetime_index): else: other = arr.tz_localize(None) - with pytest.raises(AssertionError): + with pytest.raises(ValueError, match="to_concat must have the same"): arr._concat_same_type([arr, other]) def test_concat_same_type_different_freq(self): diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py index 7a0c9300a43a2..0a5a2362bd290 100644 --- a/pandas/tests/arrays/test_integer.py +++ b/pandas/tests/arrays/test_integer.py @@ -330,26 +330,37 @@ def test_error(self, data, all_arithmetic_operators): opa = getattr(data, op) # invalid scalars - with pytest.raises(TypeError): + msg = ( + r"(:?can only perform ops with numeric values)" + r"|(:?IntegerArray cannot perform the operation mod)" + ) + with pytest.raises(TypeError, match=msg): ops("foo") - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): ops(pd.Timestamp("20180101")) # invalid array-likes - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): ops(pd.Series("foo", index=s.index)) if op != "__rpow__": # TODO(extension) # rpow with a datetimelike coerces the integer array incorrectly - with pytest.raises(TypeError): + msg = ( + "can only perform ops with numeric values|" + "cannot perform .* with this index type: DatetimeArray|" + "Addition/subtraction of integers and integer-arrays " + "with DatetimeArray is no longer supported. *" + ) + with pytest.raises(TypeError, match=msg): ops(pd.Series(pd.date_range("20180101", periods=len(s)))) # 2d result = opa(pd.DataFrame({"A": s})) assert result is NotImplemented - with pytest.raises(NotImplementedError): + msg = r"can only perform ops with 1-d structures" + with pytest.raises(NotImplementedError, match=msg): opa(np.arange(len(s)).reshape(-1, len(s))) @pytest.mark.parametrize("zero, negative", [(0, False), (0.0, False), (-0.0, True)]) @@ -589,7 +600,8 @@ def test_astype(self, all_data): # coerce to same numpy_dtype - mixed s = pd.Series(mixed) - with pytest.raises(ValueError): + msg = r"cannot convert to .*-dtype NumPy array with missing values.*" + with pytest.raises(ValueError, match=msg): s.astype(all_data.dtype.numpy_dtype) # coerce to object @@ -730,16 +742,17 @@ def test_integer_array_constructor(): expected = integer_array([1, 2, 3, np.nan], dtype="int64") tm.assert_extension_array_equal(result, expected) - with pytest.raises(TypeError): + msg = r".* should be .* numpy array. Use the 'integer_array' function instead" + with pytest.raises(TypeError, match=msg): IntegerArray(values.tolist(), mask) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): IntegerArray(values, mask.tolist()) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): IntegerArray(values.astype(float), mask) - - with pytest.raises(TypeError): + msg = r"__init__\(\) missing 1 required positional argument: 'mask'" + with pytest.raises(TypeError, match=msg): IntegerArray(values) @@ -787,7 +800,11 @@ def test_integer_array_constructor_copy(): ) def test_to_integer_array_error(values): # error in converting existing arrays to IntegerArrays - with pytest.raises(TypeError): + msg = ( + r"(:?.* cannot be converted to an IntegerDtype)" + r"|(:?values must be a 1D list-like)" + ) + with pytest.raises(TypeError, match=msg): integer_array(values) @@ -1002,7 +1019,8 @@ def test_ufuncs_binary_int(ufunc): @pytest.mark.parametrize("values", [[0, 1], [0, None]]) def test_ufunc_reduce_raises(values): a = integer_array(values) - with pytest.raises(NotImplementedError): + msg = r"The 'reduce' method is not supported." + with pytest.raises(NotImplementedError, match=msg): np.add.reduce(a) @@ -1018,9 +1036,9 @@ def test_arrow_array(data): assert arr.equals(expected) -@td.skip_if_no("pyarrow", min_version="0.15.1.dev") +@td.skip_if_no("pyarrow", min_version="0.16.0") def test_arrow_roundtrip(data): - # roundtrip possible from arrow 1.0.0 + # roundtrip possible from arrow 0.16.0 import pyarrow as pa df = pd.DataFrame({"a": data}) @@ -1030,6 +1048,19 @@ def test_arrow_roundtrip(data): tm.assert_frame_equal(result, df) +@td.skip_if_no("pyarrow", min_version="0.16.0") +def test_arrow_from_arrow_uint(): + # https://github.com/pandas-dev/pandas/issues/31896 + # possible mismatch in types + import pyarrow as pa + + dtype = pd.UInt32Dtype() + result = dtype.__from_arrow__(pa.array([1, 2, 3, 4, None], type="int64")) + expected = pd.array([1, 2, 3, 4, None], dtype="UInt32") + + tm.assert_extension_array_equal(result, expected) + + @pytest.mark.parametrize( "pandasmethname, kwargs", [ diff --git a/pandas/tests/arrays/test_period.py b/pandas/tests/arrays/test_period.py index 1f4351c7e20ee..0b95d3aa19366 100644 --- a/pandas/tests/arrays/test_period.py +++ b/pandas/tests/arrays/test_period.py @@ -371,7 +371,8 @@ def test_arrow_array(data, freq): assert result.equals(expected) # unsupported conversions - with pytest.raises(TypeError): + msg = "Not supported to convert PeriodArray to 'double' type" + with pytest.raises(TypeError, match=msg): pa.array(periods, type="float64") with pytest.raises(TypeError, match="different 'freq'"): diff --git a/pandas/tests/base/test_ops.py b/pandas/tests/base/test_ops.py index e522c7f743a05..f85d823cb2fac 100644 --- a/pandas/tests/base/test_ops.py +++ b/pandas/tests/base/test_ops.py @@ -109,26 +109,26 @@ def test_binary_ops(klass, op_name, op): assert expected_str in getattr(klass, "r" + op_name).__doc__ -class TestTranspose(Ops): +class TestTranspose: errmsg = "the 'axes' parameter is not supported" - def test_transpose(self): - for obj in self.objs: - tm.assert_equal(obj.transpose(), obj) + def test_transpose(self, index_or_series_obj): + obj = index_or_series_obj + tm.assert_equal(obj.transpose(), obj) - def test_transpose_non_default_axes(self): - for obj in self.objs: - with pytest.raises(ValueError, match=self.errmsg): - obj.transpose(1) - with pytest.raises(ValueError, match=self.errmsg): - obj.transpose(axes=1) + def test_transpose_non_default_axes(self, index_or_series_obj): + obj = index_or_series_obj + with pytest.raises(ValueError, match=self.errmsg): + obj.transpose(1) + with pytest.raises(ValueError, match=self.errmsg): + obj.transpose(axes=1) - def test_numpy_transpose(self): - for obj in self.objs: - tm.assert_equal(np.transpose(obj), obj) + def test_numpy_transpose(self, index_or_series_obj): + obj = index_or_series_obj + tm.assert_equal(np.transpose(obj), obj) - with pytest.raises(ValueError, match=self.errmsg): - np.transpose(obj, axes=1) + with pytest.raises(ValueError, match=self.errmsg): + np.transpose(obj, axes=1) class TestIndexOps(Ops): @@ -137,227 +137,244 @@ def setup_method(self, method): self.is_valid_objs = self.objs self.not_valid_objs = [] - def test_none_comparison(self): + def test_none_comparison(self, series_with_simple_index): + series = series_with_simple_index + if isinstance(series.index, IntervalIndex): + # IntervalIndex breaks on "series[0] = np.nan" below + pytest.skip("IntervalIndex doesn't support assignment") + if len(series) < 1: + pytest.skip("Test doesn't make sense on empty data") # bug brought up by #1079 # changed from TypeError in 0.17.0 - for o in self.is_valid_objs: - if isinstance(o, Series): - - o[0] = np.nan - - # noinspection PyComparisonWithNone - result = o == None # noqa - assert not result.iat[0] - assert not result.iat[1] - - # noinspection PyComparisonWithNone - result = o != None # noqa - assert result.iat[0] - assert result.iat[1] - - result = None == o # noqa - assert not result.iat[0] - assert not result.iat[1] - - result = None != o # noqa - assert result.iat[0] - assert result.iat[1] - - if is_datetime64_dtype(o) or is_datetime64tz_dtype(o): - # Following DatetimeIndex (and Timestamp) convention, - # inequality comparisons with Series[datetime64] raise - msg = "Invalid comparison" - with pytest.raises(TypeError, match=msg): - None > o - with pytest.raises(TypeError, match=msg): - o > None - else: - result = None > o - assert not result.iat[0] - assert not result.iat[1] + series[0] = np.nan + + # noinspection PyComparisonWithNone + result = series == None # noqa + assert not result.iat[0] + assert not result.iat[1] + + # noinspection PyComparisonWithNone + result = series != None # noqa + assert result.iat[0] + assert result.iat[1] + + result = None == series # noqa + assert not result.iat[0] + assert not result.iat[1] + + result = None != series # noqa + assert result.iat[0] + assert result.iat[1] + + if is_datetime64_dtype(series) or is_datetime64tz_dtype(series): + # Following DatetimeIndex (and Timestamp) convention, + # inequality comparisons with Series[datetime64] raise + msg = "Invalid comparison" + with pytest.raises(TypeError, match=msg): + None > series + with pytest.raises(TypeError, match=msg): + series > None + else: + result = None > series + assert not result.iat[0] + assert not result.iat[1] - result = o < None - assert not result.iat[0] - assert not result.iat[1] + result = series < None + assert not result.iat[0] + assert not result.iat[1] - def test_ndarray_compat_properties(self): + def test_ndarray_compat_properties(self, index_or_series_obj): + obj = index_or_series_obj - for o in self.objs: - # Check that we work. - for p in ["shape", "dtype", "T", "nbytes"]: - assert getattr(o, p, None) is not None + # Check that we work. + for p in ["shape", "dtype", "T", "nbytes"]: + assert getattr(obj, p, None) is not None - # deprecated properties - for p in ["flags", "strides", "itemsize", "base", "data"]: - assert not hasattr(o, p) + # deprecated properties + for p in ["flags", "strides", "itemsize", "base", "data"]: + assert not hasattr(obj, p) - msg = "can only convert an array of size 1 to a Python scalar" - with pytest.raises(ValueError, match=msg): - o.item() # len > 1 + msg = "can only convert an array of size 1 to a Python scalar" + with pytest.raises(ValueError, match=msg): + obj.item() # len > 1 - assert o.ndim == 1 - assert o.size == len(o) + assert obj.ndim == 1 + assert obj.size == len(obj) assert Index([1]).item() == 1 assert Series([1]).item() == 1 - def test_value_counts_unique_nunique(self): - for orig in self.objs: - o = orig.copy() - klass = type(o) - values = o._values - - if isinstance(values, Index): - # reset name not to affect latter process - values.name = None - - # create repeated values, 'n'th element is repeated by n+1 times - # skip boolean, because it only has 2 values at most - if isinstance(o, Index) and o.is_boolean(): - continue - elif isinstance(o, Index): - expected_index = Index(o[::-1]) - expected_index.name = None - o = o.repeat(range(1, len(o) + 1)) - o.name = "a" - else: - expected_index = Index(values[::-1]) - idx = o.index.repeat(range(1, len(o) + 1)) - # take-based repeat - indices = np.repeat(np.arange(len(o)), range(1, len(o) + 1)) - rep = values.take(indices) - o = klass(rep, index=idx, name="a") - - # check values has the same dtype as the original - assert o.dtype == orig.dtype - - expected_s = Series( - range(10, 0, -1), index=expected_index, dtype="int64", name="a" + def test_value_counts_unique_nunique(self, index_or_series_obj): + orig = index_or_series_obj + obj = orig.copy() + klass = type(obj) + values = obj._values + + if orig.duplicated().any(): + pytest.xfail( + "The test implementation isn't flexible enough to deal" + " with duplicated values. This isn't a bug in the" + " application code, but in the test code." ) - result = o.value_counts() - tm.assert_series_equal(result, expected_s) - assert result.index.name is None - assert result.name == "a" + # create repeated values, 'n'th element is repeated by n+1 times + if isinstance(obj, Index): + expected_index = Index(obj[::-1]) + expected_index.name = None + obj = obj.repeat(range(1, len(obj) + 1)) + else: + expected_index = Index(values[::-1]) + idx = obj.index.repeat(range(1, len(obj) + 1)) + # take-based repeat + indices = np.repeat(np.arange(len(obj)), range(1, len(obj) + 1)) + rep = values.take(indices) + obj = klass(rep, index=idx) + + # check values has the same dtype as the original + assert obj.dtype == orig.dtype + + expected_s = Series( + range(len(orig), 0, -1), index=expected_index, dtype="int64" + ) - result = o.unique() - if isinstance(o, Index): - assert isinstance(result, type(o)) - tm.assert_index_equal(result, orig) - assert result.dtype == orig.dtype - elif is_datetime64tz_dtype(o): - # datetimetz Series returns array of Timestamp - assert result[0] == orig[0] - for r in result: - assert isinstance(r, Timestamp) - - tm.assert_numpy_array_equal( - result.astype(object), orig._values.astype(object) - ) - else: - tm.assert_numpy_array_equal(result, orig.values) - assert result.dtype == orig.dtype + result = obj.value_counts() + tm.assert_series_equal(result, expected_s) + assert result.index.name is None + + result = obj.unique() + if isinstance(obj, Index): + assert isinstance(result, type(obj)) + tm.assert_index_equal(result, orig) + assert result.dtype == orig.dtype + elif is_datetime64tz_dtype(obj): + # datetimetz Series returns array of Timestamp + assert result[0] == orig[0] + for r in result: + assert isinstance(r, Timestamp) + + tm.assert_numpy_array_equal( + result.astype(object), orig._values.astype(object) + ) + else: + tm.assert_numpy_array_equal(result, orig.values) + assert result.dtype == orig.dtype - assert o.nunique() == len(np.unique(o.values)) + # dropna=True would break for MultiIndex + assert obj.nunique(dropna=False) == len(np.unique(obj.values)) @pytest.mark.parametrize("null_obj", [np.nan, None]) - def test_value_counts_unique_nunique_null(self, null_obj): - - for orig in self.objs: - o = orig.copy() - klass = type(o) - values = o._ndarray_values - - if not allow_na_ops(o): - continue - - # special assign to the numpy array - if is_datetime64tz_dtype(o): - if isinstance(o, DatetimeIndex): - v = o.asi8 - v[0:2] = iNaT - values = o._shallow_copy(v) - else: - o = o.copy() - o[0:2] = pd.NaT - values = o._values + def test_value_counts_unique_nunique_null(self, null_obj, index_or_series_obj): + orig = index_or_series_obj + obj = orig.copy() + klass = type(obj) + values = obj._ndarray_values + num_values = len(orig) + + if not allow_na_ops(obj): + pytest.skip("type doesn't allow for NA operations") + elif isinstance(orig, (pd.CategoricalIndex, pd.IntervalIndex)): + pytest.skip(f"values of {klass} cannot be changed") + elif isinstance(orig, pd.MultiIndex): + pytest.skip("MultiIndex doesn't support isna") + elif orig.duplicated().any(): + pytest.xfail( + "The test implementation isn't flexible enough to deal" + " with duplicated values. This isn't a bug in the" + " application code, but in the test code." + ) - elif needs_i8_conversion(o): - values[0:2] = iNaT - values = o._shallow_copy(values) + # special assign to the numpy array + if is_datetime64tz_dtype(obj): + if isinstance(obj, DatetimeIndex): + v = obj.asi8 + v[0:2] = iNaT + values = obj._shallow_copy(v) else: - values[0:2] = null_obj - # check values has the same dtype as the original + obj = obj.copy() + obj[0:2] = pd.NaT + values = obj._values - assert values.dtype == o.dtype + elif needs_i8_conversion(obj): + values[0:2] = iNaT + values = obj._shallow_copy(values) + else: + values[0:2] = null_obj - # create repeated values, 'n'th element is repeated by n+1 - # times - if isinstance(o, (DatetimeIndex, PeriodIndex)): - expected_index = o.copy() - expected_index.name = None + # check values has the same dtype as the original + assert values.dtype == obj.dtype - # attach name to klass - o = klass(values.repeat(range(1, len(o) + 1))) - o.name = "a" - else: - if isinstance(o, DatetimeIndex): - expected_index = orig._values._shallow_copy(values) - else: - expected_index = Index(values) - expected_index.name = None - o = o.repeat(range(1, len(o) + 1)) - o.name = "a" - - # check values has the same dtype as the original - assert o.dtype == orig.dtype - # check values correctly have NaN - nanloc = np.zeros(len(o), dtype=np.bool) - nanloc[:3] = True - if isinstance(o, Index): - tm.assert_numpy_array_equal(pd.isna(o), nanloc) - else: - exp = Series(nanloc, o.index, name="a") - tm.assert_series_equal(pd.isna(o), exp) - - expected_s_na = Series( - list(range(10, 2, -1)) + [3], - index=expected_index[9:0:-1], - dtype="int64", - name="a", - ) - expected_s = Series( - list(range(10, 2, -1)), - index=expected_index[9:1:-1], - dtype="int64", - name="a", - ) + # create repeated values, 'n'th element is repeated by n+1 + # times + if isinstance(obj, (DatetimeIndex, PeriodIndex)): + expected_index = obj.copy() + expected_index.name = None - result_s_na = o.value_counts(dropna=False) - tm.assert_series_equal(result_s_na, expected_s_na) - assert result_s_na.index.name is None - assert result_s_na.name == "a" - result_s = o.value_counts() - tm.assert_series_equal(o.value_counts(), expected_s) - assert result_s.index.name is None - assert result_s.name == "a" - - result = o.unique() - if isinstance(o, Index): - tm.assert_index_equal(result, Index(values[1:], name="a")) - elif is_datetime64tz_dtype(o): - # unable to compare NaT / nan - tm.assert_extension_array_equal(result[1:], values[2:]) - assert result[0] is pd.NaT + # attach name to klass + obj = klass(values.repeat(range(1, len(obj) + 1))) + obj.name = "a" + else: + if isinstance(obj, DatetimeIndex): + expected_index = orig._values._shallow_copy(values) else: - tm.assert_numpy_array_equal(result[1:], values[2:]) - - assert pd.isna(result[0]) - assert result.dtype == orig.dtype + expected_index = Index(values) + expected_index.name = None + obj = obj.repeat(range(1, len(obj) + 1)) + obj.name = "a" + + # check values has the same dtype as the original + assert obj.dtype == orig.dtype + + # check values correctly have NaN + nanloc = np.zeros(len(obj), dtype=np.bool) + nanloc[:3] = True + if isinstance(obj, Index): + tm.assert_numpy_array_equal(pd.isna(obj), nanloc) + else: + exp = Series(nanloc, obj.index, name="a") + tm.assert_series_equal(pd.isna(obj), exp) + + expected_data = list(range(num_values, 2, -1)) + expected_data_na = expected_data.copy() + if expected_data_na: + expected_data_na.append(3) + expected_s_na = Series( + expected_data_na, + index=expected_index[num_values - 1 : 0 : -1], + dtype="int64", + name="a", + ) + expected_s = Series( + expected_data, + index=expected_index[num_values - 1 : 1 : -1], + dtype="int64", + name="a", + ) - assert o.nunique() == 8 - assert o.nunique(dropna=False) == 9 + result_s_na = obj.value_counts(dropna=False) + tm.assert_series_equal(result_s_na, expected_s_na) + assert result_s_na.index.name is None + assert result_s_na.name == "a" + result_s = obj.value_counts() + tm.assert_series_equal(obj.value_counts(), expected_s) + assert result_s.index.name is None + assert result_s.name == "a" + + result = obj.unique() + if isinstance(obj, Index): + tm.assert_index_equal(result, Index(values[1:], name="a")) + elif is_datetime64tz_dtype(obj): + # unable to compare NaT / nan + tm.assert_extension_array_equal(result[1:], values[2:]) + assert result[0] is pd.NaT + elif len(obj) > 0: + tm.assert_numpy_array_equal(result[1:], values[2:]) + + assert pd.isna(result[0]) + assert result.dtype == orig.dtype + + assert obj.nunique() == max(0, num_values - 2) + assert obj.nunique(dropna=False) == max(0, num_values - 1) def test_value_counts_inferred(self, index_or_series): klass = index_or_series diff --git a/pandas/tests/dtypes/test_common.py b/pandas/tests/dtypes/test_common.py index 4c917b9bb42d2..8da2797835080 100644 --- a/pandas/tests/dtypes/test_common.py +++ b/pandas/tests/dtypes/test_common.py @@ -156,7 +156,6 @@ def get_is_dtype_funcs(): begin with 'is_' and end with 'dtype' """ - fnames = [f for f in dir(com) if (f.startswith("is_") and f.endswith("dtype"))] return [getattr(com, fname) for fname in fnames] diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index dd99b81fb6764..9eb5fda87d2d2 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -127,6 +127,11 @@ def test_from_values_or_dtype_raises(self, values, categories, ordered, dtype): with pytest.raises(ValueError, match=msg): CategoricalDtype._from_values_or_dtype(values, categories, ordered, dtype) + def test_from_values_or_dtype_invalid_dtype(self): + msg = "Cannot not construct CategoricalDtype from " + with pytest.raises(ValueError, match=msg): + CategoricalDtype._from_values_or_dtype(None, None, None, object) + def test_is_dtype(self, dtype): assert CategoricalDtype.is_dtype(dtype) assert CategoricalDtype.is_dtype("category") diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index 48f9262ad3486..48ae1f67297af 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -1200,6 +1200,24 @@ def test_interval(self): inferred = lib.infer_dtype(pd.Series(idx), skipna=False) assert inferred == "interval" + @pytest.mark.parametrize("klass", [pd.array, pd.Series]) + @pytest.mark.parametrize("skipna", [True, False]) + @pytest.mark.parametrize("data", [["a", "b", "c"], ["a", "b", pd.NA]]) + def test_string_dtype(self, data, skipna, klass): + # StringArray + val = klass(data, dtype="string") + inferred = lib.infer_dtype(val, skipna=skipna) + assert inferred == "string" + + @pytest.mark.parametrize("klass", [pd.array, pd.Series]) + @pytest.mark.parametrize("skipna", [True, False]) + @pytest.mark.parametrize("data", [[True, False, True], [True, False, pd.NA]]) + def test_boolean_dtype(self, data, skipna, klass): + # BooleanArray + val = klass(data, dtype="boolean") + inferred = lib.infer_dtype(val, skipna=skipna) + assert inferred == "boolean" + class TestNumberScalar: def test_is_number(self): diff --git a/pandas/tests/extension/arrow/arrays.py b/pandas/tests/extension/arrow/arrays.py index b67ca4cfab83d..ffebc9f8b3359 100644 --- a/pandas/tests/extension/arrow/arrays.py +++ b/pandas/tests/extension/arrow/arrays.py @@ -1,4 +1,5 @@ -"""Rudimentary Apache Arrow-backed ExtensionArray. +""" +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 @@ -147,8 +148,8 @@ def _reduce(self, method, skipna=True, **kwargs): try: op = getattr(arr, method) - except AttributeError: - raise TypeError + except AttributeError as err: + raise TypeError from err return op(**kwargs) def any(self, axis=0, out=None): diff --git a/pandas/tests/extension/base/__init__.py b/pandas/tests/extension/base/__init__.py index e2b6ea0304f6a..323cb843b2d74 100644 --- a/pandas/tests/extension/base/__init__.py +++ b/pandas/tests/extension/base/__init__.py @@ -1,4 +1,5 @@ -"""Base test suite for extension arrays. +""" +Base test suite for extension arrays. These tests are intended for third-party libraries to subclass to validate that their extension arrays and dtypes satisfy the interface. Moving or diff --git a/pandas/tests/extension/base/base.py b/pandas/tests/extension/base/base.py index 144b0825b39a2..97d8e7c66dbdb 100644 --- a/pandas/tests/extension/base/base.py +++ b/pandas/tests/extension/base/base.py @@ -2,8 +2,20 @@ class BaseExtensionTests: + # classmethod and different signature is needed + # to make inheritance compliant with mypy + @classmethod + def assert_equal(cls, left, right, **kwargs): + return tm.assert_equal(left, right, **kwargs) - assert_equal = staticmethod(tm.assert_equal) - assert_series_equal = staticmethod(tm.assert_series_equal) - assert_frame_equal = staticmethod(tm.assert_frame_equal) - assert_extension_array_equal = staticmethod(tm.assert_extension_array_equal) + @classmethod + def assert_series_equal(cls, left, right, *args, **kwargs): + return tm.assert_series_equal(left, right, *args, **kwargs) + + @classmethod + def assert_frame_equal(cls, left, right, *args, **kwargs): + return tm.assert_frame_equal(left, right, *args, **kwargs) + + @classmethod + def assert_extension_array_equal(cls, left, right, *args, **kwargs): + return tm.assert_extension_array_equal(left, right, *args, **kwargs) diff --git a/pandas/tests/extension/base/getitem.py b/pandas/tests/extension/base/getitem.py index 8615a8df22dcc..b08a64cc076b6 100644 --- a/pandas/tests/extension/base/getitem.py +++ b/pandas/tests/extension/base/getitem.py @@ -158,21 +158,23 @@ def test_getitem_boolean_array_mask(self, data): result = pd.Series(data)[mask] self.assert_series_equal(result, expected) - def test_getitem_boolean_array_mask_raises(self, data): + def test_getitem_boolean_na_treated_as_false(self, data): + # https://github.com/pandas-dev/pandas/issues/31503 mask = pd.array(np.zeros(data.shape, dtype="bool"), dtype="boolean") mask[:2] = pd.NA + mask[2:4] = True - msg = ( - "Cannot mask with a boolean indexer containing NA values|" - "cannot mask with array containing NA / NaN values" - ) - with pytest.raises(ValueError, match=msg): - data[mask] + result = data[mask] + expected = data[mask.fillna(False)] + + self.assert_extension_array_equal(result, expected) s = pd.Series(data) - with pytest.raises(ValueError): - s[mask] + result = s[mask] + expected = s[mask.fillna(False)] + + self.assert_series_equal(result, expected) @pytest.mark.parametrize( "idx", diff --git a/pandas/tests/extension/base/ops.py b/pandas/tests/extension/base/ops.py index 0609f19c8e0c3..4009041218ac2 100644 --- a/pandas/tests/extension/base/ops.py +++ b/pandas/tests/extension/base/ops.py @@ -51,7 +51,8 @@ def _check_divmod_op(self, s, op, other, exc=Exception): class BaseArithmeticOpsTests(BaseOpsUtil): - """Various Series and DataFrame arithmetic ops methods. + """ + Various Series and DataFrame arithmetic ops methods. Subclasses supporting various ops should set the class variables to indicate that they support ops of that kind diff --git a/pandas/tests/extension/base/setitem.py b/pandas/tests/extension/base/setitem.py index e0ca603aaa0ed..a4fe89df158fa 100644 --- a/pandas/tests/extension/base/setitem.py +++ b/pandas/tests/extension/base/setitem.py @@ -4,7 +4,7 @@ import pytest import pandas as pd -from pandas.core.arrays.numpy_ import PandasDtype +import pandas._testing as tm from .base import BaseExtensionTests @@ -93,6 +93,90 @@ def test_setitem_iloc_scalar_multiple_homogoneous(self, data): df.iloc[10, 1] = data[1] assert df.loc[10, "B"] == data[1] + @pytest.mark.parametrize( + "mask", + [ + np.array([True, True, True, False, False]), + pd.array([True, True, True, False, False], dtype="boolean"), + pd.array([True, True, True, pd.NA, pd.NA], dtype="boolean"), + ], + ids=["numpy-array", "boolean-array", "boolean-array-na"], + ) + def test_setitem_mask(self, data, mask, box_in_series): + arr = data[:5].copy() + expected = arr.take([0, 0, 0, 3, 4]) + if box_in_series: + arr = pd.Series(arr) + expected = pd.Series(expected) + arr[mask] = data[0] + self.assert_equal(expected, arr) + + def test_setitem_mask_raises(self, data, box_in_series): + # wrong length + mask = np.array([True, False]) + + if box_in_series: + data = pd.Series(data) + + with pytest.raises(IndexError, match="wrong length"): + data[mask] = data[0] + + mask = pd.array(mask, dtype="boolean") + with pytest.raises(IndexError, match="wrong length"): + data[mask] = data[0] + + def test_setitem_mask_boolean_array_with_na(self, data, box_in_series): + mask = pd.array(np.zeros(data.shape, dtype="bool"), dtype="boolean") + mask[:3] = True + mask[3:5] = pd.NA + + if box_in_series: + data = pd.Series(data) + + data[mask] = data[0] + + assert (data[:3] == data[0]).all() + + @pytest.mark.parametrize( + "idx", + [[0, 1, 2], pd.array([0, 1, 2], dtype="Int64"), np.array([0, 1, 2])], + ids=["list", "integer-array", "numpy-array"], + ) + def test_setitem_integer_array(self, data, idx, box_in_series): + arr = data[:5].copy() + expected = data.take([0, 0, 0, 3, 4]) + + if box_in_series: + arr = pd.Series(arr) + expected = pd.Series(expected) + + arr[idx] = arr[0] + self.assert_equal(arr, expected) + + @pytest.mark.parametrize( + "idx, box_in_series", + [ + ([0, 1, 2, pd.NA], False), + pytest.param( + [0, 1, 2, pd.NA], True, marks=pytest.mark.xfail(reason="GH-31948") + ), + (pd.array([0, 1, 2, pd.NA], dtype="Int64"), False), + (pd.array([0, 1, 2, pd.NA], dtype="Int64"), False), + ], + ids=["list-False", "list-True", "integer-array-False", "integer-array-True"], + ) + def test_setitem_integer_with_missing_raises(self, data, idx, box_in_series): + arr = data.copy() + + # TODO(xfail) this raises KeyError about labels not found (it tries label-based) + # for list of labels with Series + if box_in_series: + arr = pd.Series(data, index=[tm.rands(4) for _ in range(len(data))]) + + msg = "Cannot index with an integer indexer containing NA values" + with pytest.raises(ValueError, match=msg): + arr[idx] = arr[0] + @pytest.mark.parametrize("as_callable", [True, False]) @pytest.mark.parametrize("setter", ["loc", None]) def test_setitem_mask_aligned(self, data, as_callable, setter): @@ -173,6 +257,29 @@ def test_setitem_tuple_index(self, data): s[(0, 1)] = data[1] self.assert_series_equal(s, expected) + def test_setitem_slice(self, data, box_in_series): + arr = data[:5].copy() + expected = data.take([0, 0, 0, 3, 4]) + if box_in_series: + arr = pd.Series(arr) + expected = pd.Series(expected) + + arr[:3] = data[0] + self.assert_equal(arr, expected) + + def test_setitem_loc_iloc_slice(self, data): + arr = data[:5].copy() + s = pd.Series(arr, index=["a", "b", "c", "d", "e"]) + expected = pd.Series(data.take([0, 0, 0, 3, 4]), index=s.index) + + result = s.copy() + result.iloc[:3] = data[0] + self.assert_equal(result, expected) + + result = s.copy() + result.loc[:"c"] = data[0] + self.assert_equal(result, expected) + def test_setitem_slice_mismatch_length_raises(self, data): arr = data[:5] with pytest.raises(ValueError): @@ -196,14 +303,3 @@ def test_setitem_preserves_views(self, data): data[0] = data[1] assert view1[0] == data[1] assert view2[0] == data[1] - - def test_setitem_nullable_mask(self, data): - # GH 31446 - # TODO: there is some issue with PandasArray, therefore, - # TODO: skip the setitem test for now, and fix it later - if data.dtype != PandasDtype("object"): - arr = data[:5] - expected = data.take([0, 0, 0, 3, 4]) - mask = pd.array([True, True, True, False, False]) - arr[mask] = data[0] - self.assert_extension_array_equal(expected, arr) diff --git a/pandas/tests/extension/conftest.py b/pandas/tests/extension/conftest.py index d37638d37e4d6..1942d737780da 100644 --- a/pandas/tests/extension/conftest.py +++ b/pandas/tests/extension/conftest.py @@ -13,7 +13,8 @@ def dtype(): @pytest.fixture def data(): - """Length-100 array for this type. + """ + Length-100 array for this type. * data[0] and data[1] should both be non missing * data[0] and data[1] should not be equal @@ -67,7 +68,8 @@ def gen(count): @pytest.fixture def data_for_sorting(): - """Length-3 array with a known sort order. + """ + Length-3 array with a known sort order. This should be three items [B, C, A] with A < B < C @@ -77,7 +79,8 @@ def data_for_sorting(): @pytest.fixture def data_missing_for_sorting(): - """Length-3 array with a known sort order. + """ + Length-3 array with a known sort order. This should be three items [B, NA, A] with A < B and NA missing. @@ -87,7 +90,8 @@ def data_missing_for_sorting(): @pytest.fixture def na_cmp(): - """Binary operator for comparing NA values. + """ + Binary operator for comparing NA values. Should return a function of two arguments that returns True if both arguments are (scalar) NA for your type. @@ -105,7 +109,8 @@ def na_value(): @pytest.fixture def data_for_grouping(): - """Data for factorization, grouping, and unique tests. + """ + Data for factorization, grouping, and unique tests. Expected to be like [B, B, NA, NA, A, A, B, C] diff --git a/pandas/tests/extension/decimal/array.py b/pandas/tests/extension/decimal/array.py index 2614d8c72c342..9384ed5199c1f 100644 --- a/pandas/tests/extension/decimal/array.py +++ b/pandas/tests/extension/decimal/array.py @@ -183,8 +183,10 @@ def _reduce(self, name, skipna=True, **kwargs): try: op = getattr(self.data, name) - except AttributeError: - raise NotImplementedError(f"decimal does not support the {name} operation") + except AttributeError as err: + raise NotImplementedError( + f"decimal does not support the {name} operation" + ) from err return op(axis=0) diff --git a/pandas/tests/extension/decimal/test_decimal.py b/pandas/tests/extension/decimal/test_decimal.py index de7c98ab96571..f4ffcb8d0f109 100644 --- a/pandas/tests/extension/decimal/test_decimal.py +++ b/pandas/tests/extension/decimal/test_decimal.py @@ -66,7 +66,8 @@ def data_for_grouping(): class BaseDecimal: - def assert_series_equal(self, left, right, *args, **kwargs): + @classmethod + def assert_series_equal(cls, left, right, *args, **kwargs): def convert(x): # need to convert array([Decimal(NaN)], dtype='object') to np.NaN # because Series[object].isnan doesn't recognize decimal(NaN) as @@ -88,7 +89,8 @@ def convert(x): tm.assert_series_equal(left_na, right_na) return tm.assert_series_equal(left[~left_na], right[~right_na], *args, **kwargs) - def assert_frame_equal(self, left, right, *args, **kwargs): + @classmethod + def assert_frame_equal(cls, left, right, *args, **kwargs): # TODO(EA): select_dtypes tm.assert_index_equal( left.columns, @@ -97,13 +99,13 @@ def assert_frame_equal(self, left, right, *args, **kwargs): check_names=kwargs.get("check_names", True), check_exact=kwargs.get("check_exact", False), check_categorical=kwargs.get("check_categorical", True), - obj="{obj}.columns".format(obj=kwargs.get("obj", "DataFrame")), + obj=f"{kwargs.get('obj', 'DataFrame')}.columns", ) decimals = (left.dtypes == "decimal").index for col in decimals: - self.assert_series_equal(left[col], right[col], *args, **kwargs) + cls.assert_series_equal(left[col], right[col], *args, **kwargs) left = left.drop(columns=decimals) right = right.drop(columns=decimals) @@ -146,7 +148,8 @@ class Reduce: def check_reduce(self, s, op_name, skipna): if op_name in ["median", "skew", "kurt"]: - with pytest.raises(NotImplementedError): + msg = r"decimal does not support the .* operation" + with pytest.raises(NotImplementedError, match=msg): getattr(s, op_name)(skipna=skipna) else: diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py index 1ba1b872fa5e2..1f026e405dc17 100644 --- a/pandas/tests/extension/json/array.py +++ b/pandas/tests/extension/json/array.py @@ -1,10 +1,11 @@ -"""Test extension array for storing nested data in a pandas container. +""" +Test extension array for storing nested data in a pandas container. The JSONArray stores lists of dictionaries. The storage mechanism is a list, not an ndarray. -Note: - +Note +---- We currently store lists of UserDicts. Pandas has a few places internally that specifically check for dicts, and does non-scalar things in that case. We *want* the dictionaries to be treated as scalars, so we @@ -136,13 +137,13 @@ def take(self, indexer, allow_fill=False, fill_value=None): output = [ self.data[loc] if loc != -1 else fill_value for loc in indexer ] - except IndexError: - raise IndexError(msg) + except IndexError as err: + raise IndexError(msg) from err else: try: output = [self.data[loc] for loc in indexer] - except IndexError: - raise IndexError(msg) + except IndexError as err: + raise IndexError(msg) from err return self._from_sequence(output) diff --git a/pandas/tests/extension/json/test_json.py b/pandas/tests/extension/json/test_json.py index dc03a1f1dcf72..d086896fb09c3 100644 --- a/pandas/tests/extension/json/test_json.py +++ b/pandas/tests/extension/json/test_json.py @@ -79,7 +79,8 @@ class BaseJSON: # The default assert_series_equal eventually does a # Series.values, which raises. We work around it by # converting the UserDicts to dicts. - def assert_series_equal(self, left, right, **kwargs): + @classmethod + def assert_series_equal(cls, left, right, *args, **kwargs): if left.dtype.name == "json": assert left.dtype == right.dtype left = pd.Series( @@ -90,9 +91,10 @@ def assert_series_equal(self, left, right, **kwargs): index=right.index, name=right.name, ) - tm.assert_series_equal(left, right, **kwargs) + tm.assert_series_equal(left, right, *args, **kwargs) - def assert_frame_equal(self, left, right, *args, **kwargs): + @classmethod + def assert_frame_equal(cls, left, right, *args, **kwargs): obj_type = kwargs.get("obj", "DataFrame") tm.assert_index_equal( left.columns, @@ -107,7 +109,7 @@ def assert_frame_equal(self, left, right, *args, **kwargs): jsons = (left.dtypes == "json").index for col in jsons: - self.assert_series_equal(left[col], right[col], *args, **kwargs) + cls.assert_series_equal(left[col], right[col], *args, **kwargs) left = left.drop(columns=jsons) right = right.drop(columns=jsons) @@ -134,10 +136,11 @@ def test_custom_asserts(self): self.assert_frame_equal(a.to_frame(), a.to_frame()) b = pd.Series(data.take([0, 0, 1])) - with pytest.raises(AssertionError): + msg = r"ExtensionArray are different" + with pytest.raises(AssertionError, match=msg): self.assert_series_equal(a, b) - with pytest.raises(AssertionError): + with pytest.raises(AssertionError, match=msg): self.assert_frame_equal(a.to_frame(), b.to_frame()) diff --git a/pandas/tests/extension/list/array.py b/pandas/tests/extension/list/array.py index 7c1da5e8102e2..d86f90e58d897 100644 --- a/pandas/tests/extension/list/array.py +++ b/pandas/tests/extension/list/array.py @@ -86,13 +86,13 @@ def take(self, indexer, allow_fill=False, fill_value=None): output = [ self.data[loc] if loc != -1 else fill_value for loc in indexer ] - except IndexError: - raise IndexError(msg) + except IndexError as err: + raise IndexError(msg) from err else: try: output = [self.data[loc] for loc in indexer] - except IndexError: - raise IndexError(msg) + except IndexError as err: + raise IndexError(msg) from err return self._from_sequence(output) diff --git a/pandas/tests/extension/test_boolean.py b/pandas/tests/extension/test_boolean.py index 0c6b187eac1fc..e2331b69916fb 100644 --- a/pandas/tests/extension/test_boolean.py +++ b/pandas/tests/extension/test_boolean.py @@ -112,9 +112,9 @@ def _check_op(self, s, op, other, op_name, exc=NotImplementedError): # subtraction for bools raises TypeError (but not yet in 1.13) if _np_version_under1p14: pytest.skip("__sub__ does not yet raise in numpy 1.13") - with pytest.raises(TypeError): + msg = r"numpy boolean subtract" + with pytest.raises(TypeError, match=msg): op(s, other) - return result = op(s, other) diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py index 336b23e54d74c..69a97f5c9fe02 100644 --- a/pandas/tests/extension/test_categorical.py +++ b/pandas/tests/extension/test_categorical.py @@ -278,7 +278,8 @@ def _compare_other(self, s, data, op_name, other): assert (result == expected).all() else: - with pytest.raises(TypeError): + msg = "Unordered Categoricals can only compare equality or not" + with pytest.raises(TypeError, match=msg): op(data, other) diff --git a/pandas/tests/extension/test_datetime.py b/pandas/tests/extension/test_datetime.py index a60607d586ada..3aa188098620d 100644 --- a/pandas/tests/extension/test_datetime.py +++ b/pandas/tests/extension/test_datetime.py @@ -44,9 +44,9 @@ def data_missing_for_sorting(dtype): @pytest.fixture def data_for_grouping(dtype): """ - Expected to be like [B, B, NA, NA, A, A, B, C] + Expected to be like [B, B, NA, NA, A, A, B, C] - Where A < B < C and NA is missing + Where A < B < C and NA is missing """ a = pd.Timestamp("2000-01-01") b = pd.Timestamp("2000-01-02") diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py index 8a820c8746857..61c5925383f88 100644 --- a/pandas/tests/extension/test_numpy.py +++ b/pandas/tests/extension/test_numpy.py @@ -396,6 +396,56 @@ def test_setitem_scalar_key_sequence_raise(self, data): # Failed: DID NOT RAISE super().test_setitem_scalar_key_sequence_raise(data) + # TODO: there is some issue with PandasArray, therefore, + # skip the setitem test for now, and fix it later (GH 31446) + + @skip_nested + @pytest.mark.parametrize( + "mask", + [ + np.array([True, True, True, False, False]), + pd.array([True, True, True, False, False], dtype="boolean"), + ], + ids=["numpy-array", "boolean-array"], + ) + def test_setitem_mask(self, data, mask, box_in_series): + super().test_setitem_mask(data, mask, box_in_series) + + @skip_nested + def test_setitem_mask_raises(self, data, box_in_series): + super().test_setitem_mask_raises(data, box_in_series) + + @skip_nested + @pytest.mark.parametrize( + "idx", + [[0, 1, 2], pd.array([0, 1, 2], dtype="Int64"), np.array([0, 1, 2])], + ids=["list", "integer-array", "numpy-array"], + ) + def test_setitem_integer_array(self, data, idx, box_in_series): + super().test_setitem_integer_array(data, idx, box_in_series) + + @skip_nested + @pytest.mark.parametrize( + "idx, box_in_series", + [ + ([0, 1, 2, pd.NA], False), + pytest.param([0, 1, 2, pd.NA], True, marks=pytest.mark.xfail), + (pd.array([0, 1, 2, pd.NA], dtype="Int64"), False), + (pd.array([0, 1, 2, pd.NA], dtype="Int64"), False), + ], + ids=["list-False", "list-True", "integer-array-False", "integer-array-True"], + ) + def test_setitem_integer_with_missing_raises(self, data, idx, box_in_series): + super().test_setitem_integer_with_missing_raises(data, idx, box_in_series) + + @skip_nested + def test_setitem_slice(self, data, box_in_series): + super().test_setitem_slice(data, box_in_series) + + @skip_nested + def test_setitem_loc_iloc_slice(self, data): + super().test_setitem_loc_iloc_slice(data) + @skip_nested class TestParsing(BaseNumPyTests, base.BaseParsingTests): diff --git a/pandas/tests/frame/conftest.py b/pandas/tests/frame/conftest.py index 774eb443c45fe..03598b6bb5eca 100644 --- a/pandas/tests/frame/conftest.py +++ b/pandas/tests/frame/conftest.py @@ -1,3 +1,5 @@ +from itertools import product + import numpy as np import pytest @@ -5,6 +7,11 @@ import pandas._testing as tm +@pytest.fixture(params=product([True, False], [True, False])) +def close_open_fixture(request): + return request.param + + @pytest.fixture def float_frame_with_na(): """ diff --git a/pandas/tests/frame/indexing/test_categorical.py b/pandas/tests/frame/indexing/test_categorical.py index a29c193676db2..f5b3f980cc534 100644 --- a/pandas/tests/frame/indexing/test_categorical.py +++ b/pandas/tests/frame/indexing/test_categorical.py @@ -14,9 +14,7 @@ def test_assignment(self): df = DataFrame( {"value": np.array(np.random.randint(0, 10000, 100), dtype="int32")} ) - labels = Categorical( - ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] - ) + labels = Categorical([f"{i} - {i + 499}" for i in range(0, 10000, 500)]) df = df.sort_values(by=["value"], ascending=True) s = pd.cut(df.value, range(0, 10500, 500), right=False, labels=labels) @@ -117,7 +115,12 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_cats_value) # - assign a single value not in the current categories set - with pytest.raises(ValueError): + msg1 = ( + "Cannot setitem on a Categorical with a new category, " + "set the categories first" + ) + msg2 = "Cannot set a Categorical with another, without identical categories" + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.iloc[2, 0] = "c" @@ -127,7 +130,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_row) # - assign a complete row (mixed values) not in categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.iloc[2, :] = ["c", 2] @@ -136,7 +139,7 @@ def test_assigning_ops(self): df.iloc[2:4, :] = [["b", 2], ["b", 2]] tm.assert_frame_equal(df, exp_multi_row) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.iloc[2:4, :] = [["c", 2], ["c", 2]] @@ -146,12 +149,12 @@ def test_assigning_ops(self): df.iloc[2:4, 0] = Categorical(["b", "b"], categories=["a", "b"]) tm.assert_frame_equal(df, exp_parts_cats_col) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg2): # different categories -> not sure if this should fail or pass df = orig.copy() df.iloc[2:4, 0] = Categorical(list("bb"), categories=list("abc")) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg2): # different values df = orig.copy() df.iloc[2:4, 0] = Categorical(list("cc"), categories=list("abc")) @@ -162,7 +165,7 @@ def test_assigning_ops(self): df.iloc[2:4, 0] = ["b", "b"] tm.assert_frame_equal(df, exp_parts_cats_col) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df.iloc[2:4, 0] = ["c", "c"] # loc @@ -177,7 +180,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_cats_value) # - assign a single value not in the current categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.loc["j", "cats"] = "c" @@ -187,7 +190,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_row) # - assign a complete row (mixed values) not in categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.loc["j", :] = ["c", 2] @@ -196,7 +199,7 @@ def test_assigning_ops(self): df.loc["j":"k", :] = [["b", 2], ["b", 2]] tm.assert_frame_equal(df, exp_multi_row) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.loc["j":"k", :] = [["c", 2], ["c", 2]] @@ -206,14 +209,14 @@ def test_assigning_ops(self): df.loc["j":"k", "cats"] = Categorical(["b", "b"], categories=["a", "b"]) tm.assert_frame_equal(df, exp_parts_cats_col) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg2): # different categories -> not sure if this should fail or pass df = orig.copy() df.loc["j":"k", "cats"] = Categorical( ["b", "b"], categories=["a", "b", "c"] ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg2): # different values df = orig.copy() df.loc["j":"k", "cats"] = Categorical( @@ -226,7 +229,7 @@ def test_assigning_ops(self): df.loc["j":"k", "cats"] = ["b", "b"] tm.assert_frame_equal(df, exp_parts_cats_col) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df.loc["j":"k", "cats"] = ["c", "c"] # loc @@ -241,7 +244,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_cats_value) # - assign a single value not in the current categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.loc["j", df.columns[0]] = "c" @@ -251,7 +254,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_row) # - assign a complete row (mixed values) not in categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.loc["j", :] = ["c", 2] @@ -260,7 +263,7 @@ def test_assigning_ops(self): df.loc["j":"k", :] = [["b", 2], ["b", 2]] tm.assert_frame_equal(df, exp_multi_row) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.loc["j":"k", :] = [["c", 2], ["c", 2]] @@ -270,14 +273,14 @@ def test_assigning_ops(self): df.loc["j":"k", df.columns[0]] = Categorical(["b", "b"], categories=["a", "b"]) tm.assert_frame_equal(df, exp_parts_cats_col) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg2): # different categories -> not sure if this should fail or pass df = orig.copy() df.loc["j":"k", df.columns[0]] = Categorical( ["b", "b"], categories=["a", "b", "c"] ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg2): # different values df = orig.copy() df.loc["j":"k", df.columns[0]] = Categorical( @@ -290,7 +293,7 @@ def test_assigning_ops(self): df.loc["j":"k", df.columns[0]] = ["b", "b"] tm.assert_frame_equal(df, exp_parts_cats_col) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df.loc["j":"k", df.columns[0]] = ["c", "c"] # iat @@ -299,7 +302,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_cats_value) # - assign a single value not in the current categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.iat[2, 0] = "c" @@ -310,7 +313,7 @@ def test_assigning_ops(self): tm.assert_frame_equal(df, exp_single_cats_value) # - assign a single value not in the current categories set - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.at["j", "cats"] = "c" @@ -334,7 +337,7 @@ def test_assigning_ops(self): df.at["j", "cats"] = "b" tm.assert_frame_equal(df, exp_single_cats_value) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg1): df = orig.copy() df.at["j", "cats"] = "c" @@ -348,7 +351,7 @@ def test_assigning_ops(self): def test_functions_no_warnings(self): df = DataFrame({"value": np.random.randint(0, 100, 20)}) - labels = ["{0} - {1}".format(i, i + 9) for i in range(0, 100, 10)] + labels = [f"{i} - {i + 9}" for i in range(0, 100, 10)] with tm.assert_produces_warning(False): df["group"] = pd.cut( df.value, range(0, 105, 10), right=False, labels=labels diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 6fc8c0e9ad459..997414eceeb86 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -27,6 +27,9 @@ from pandas.tseries.offsets import BDay +# We pass through a TypeError raised by numpy +_slice_msg = "slice indices must be integers or None or have an __index__ method" + class TestGet: def test_get(self, float_frame): @@ -94,6 +97,14 @@ def test_loc_iterable(self, float_frame, key_type): expected = float_frame.loc[:, ["A", "B", "C"]] tm.assert_frame_equal(result, expected) + def test_loc_timedelta_0seconds(self): + # GH#10583 + df = pd.DataFrame(np.random.normal(size=(10, 4))) + df.index = pd.timedelta_range(start="0s", periods=10, freq="s") + expected = df.loc[pd.Timedelta("0s") :, :] + result = df.loc["0s":, :] + tm.assert_frame_equal(expected, result) + @pytest.mark.parametrize( "idx_type", [ @@ -204,7 +215,7 @@ def test_setitem_list_of_tuples(self, float_frame): expected = Series(tuples, index=float_frame.index, name="tuples") tm.assert_series_equal(result, expected) - def test_setitem_mulit_index(self): + def test_setitem_multi_index(self): # GH7655, test that assigning to a sub-frame of a frame # with multi-index columns aligns both rows and columns it = ["jim", "joe", "jolie"], ["first", "last"], ["left", "center", "right"] @@ -473,7 +484,8 @@ def test_setitem(self, float_frame): # so raise/warn smaller = float_frame[:2] - with pytest.raises(com.SettingWithCopyError): + msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" + with pytest.raises(com.SettingWithCopyError, match=msg): smaller["col10"] = ["1", "2"] assert smaller["col10"].dtype == np.object_ @@ -857,7 +869,8 @@ def test_fancy_getitem_slice_mixed(self, float_frame, float_string_frame): # setting it triggers setting with copy sliced = float_frame.iloc[:, -3:] - with pytest.raises(com.SettingWithCopyError): + msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" + with pytest.raises(com.SettingWithCopyError, match=msg): sliced["C"] = 4.0 assert (float_frame["C"] == 4).all() @@ -984,7 +997,8 @@ def test_getitem_setitem_fancy_exceptions(self, float_frame): with pytest.raises(IndexingError, match="Too many indexers"): ix[:, :, :] - with pytest.raises(IndexingError): + with pytest.raises(IndexError, match="too many indices for array"): + # GH#32257 we let numpy do validation, get their exception ix[:, :, :] = 1 def test_getitem_setitem_boolean_misaligned(self, float_frame): @@ -1063,10 +1077,10 @@ def test_getitem_setitem_float_labels(self): cp = df.copy() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=_slice_msg): cp.iloc[1.0:5] = 0 - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): result = cp.iloc[1.0:5] == 0 # noqa assert result.values.all() @@ -1369,28 +1383,28 @@ def test_set_value(self, float_frame): def test_set_value_resize(self, float_frame): res = float_frame._set_value("foobar", "B", 0) - assert res is float_frame - assert res.index[-1] == "foobar" - assert res._get_value("foobar", "B") == 0 + assert res is None + assert float_frame.index[-1] == "foobar" + assert float_frame._get_value("foobar", "B") == 0 float_frame.loc["foobar", "qux"] = 0 assert float_frame._get_value("foobar", "qux") == 0 res = float_frame.copy() - res3 = res._set_value("foobar", "baz", "sam") - assert res3["baz"].dtype == np.object_ + res._set_value("foobar", "baz", "sam") + assert res["baz"].dtype == np.object_ res = float_frame.copy() - res3 = res._set_value("foobar", "baz", True) - assert res3["baz"].dtype == np.object_ + res._set_value("foobar", "baz", True) + assert res["baz"].dtype == np.object_ res = float_frame.copy() - res3 = res._set_value("foobar", "baz", 5) - assert is_float_dtype(res3["baz"]) - assert isna(res3["baz"].drop(["foobar"])).all() + res._set_value("foobar", "baz", 5) + assert is_float_dtype(res["baz"]) + assert isna(res["baz"].drop(["foobar"])).all() msg = "could not convert string to float: 'sam'" with pytest.raises(ValueError, match=msg): - res3._set_value("foobar", "baz", "sam") + res._set_value("foobar", "baz", "sam") def test_set_value_with_index_dtype_change(self): df_orig = DataFrame(np.random.randn(3, 3), index=range(3), columns=list("ABC")) @@ -1462,7 +1476,8 @@ def test_iloc_row(self): # verify slice is view # setting it makes it raise/warn - with pytest.raises(com.SettingWithCopyError): + msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" + with pytest.raises(com.SettingWithCopyError, match=msg): result[2] = 0.0 exp_col = df[2].copy() @@ -1493,7 +1508,8 @@ def test_iloc_col(self): # verify slice is view # and that we are setting a copy - with pytest.raises(com.SettingWithCopyError): + msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" + with pytest.raises(com.SettingWithCopyError, match=msg): result[8] = 0.0 assert (df[8] == 0).all() @@ -1611,6 +1627,14 @@ def test_reindex_nearest_tz(self, tz_aware_fixture): actual = df.reindex(idx[:3], method="nearest") tm.assert_frame_equal(expected, actual) + def test_reindex_nearest_tz_empty_frame(self): + # https://github.com/pandas-dev/pandas/issues/31964 + dti = pd.DatetimeIndex(["2016-06-26 14:27:26+00:00"]) + df = pd.DataFrame(index=pd.DatetimeIndex(["2016-07-04 14:00:59+00:00"])) + expected = pd.DataFrame(index=dti) + result = df.reindex(dti, method="nearest") + tm.assert_frame_equal(result, expected) + def test_reindex_frame_add_nat(self): rng = date_range("1/1/2000 00:00:00", periods=10, freq="10s") df = DataFrame({"A": np.random.randn(len(rng)), "B": rng}) diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py index 507b2e9cd237b..eee754a47fb8c 100644 --- a/pandas/tests/frame/indexing/test_where.py +++ b/pandas/tests/frame/indexing/test_where.py @@ -50,7 +50,8 @@ def _check_get(df, cond, check_dtypes=True): # check getting df = where_frame if df is float_string_frame: - with pytest.raises(TypeError): + msg = "'>' not supported between instances of 'str' and 'int'" + with pytest.raises(TypeError, match=msg): df > 0 return cond = df > 0 @@ -114,7 +115,8 @@ def _check_align(df, cond, other, check_dtypes=True): df = where_frame if df is float_string_frame: - with pytest.raises(TypeError): + msg = "'>' not supported between instances of 'str' and 'int'" + with pytest.raises(TypeError, match=msg): df > 0 return @@ -172,7 +174,8 @@ def _check_set(df, cond, check_dtypes=True): df = where_frame if df is float_string_frame: - with pytest.raises(TypeError): + msg = "'>' not supported between instances of 'str' and 'int'" + with pytest.raises(TypeError, match=msg): df > 0 return @@ -358,7 +361,8 @@ def test_where_datetime(self): ) stamp = datetime(2013, 1, 3) - with pytest.raises(TypeError): + msg = "'>' not supported between instances of 'float' and 'datetime.datetime'" + with pytest.raises(TypeError, match=msg): df > stamp result = df[df.iloc[:, :-1] > stamp] diff --git a/pandas/tests/frame/methods/test_asfreq.py b/pandas/tests/frame/methods/test_asfreq.py new file mode 100644 index 0000000000000..40b0ec0c0d811 --- /dev/null +++ b/pandas/tests/frame/methods/test_asfreq.py @@ -0,0 +1,58 @@ +from datetime import datetime + +import numpy as np + +from pandas import DataFrame, DatetimeIndex, Series, date_range +import pandas._testing as tm + +from pandas.tseries import offsets + + +class TestAsFreq: + def test_asfreq(self, datetime_frame): + offset_monthly = datetime_frame.asfreq(offsets.BMonthEnd()) + rule_monthly = datetime_frame.asfreq("BM") + + tm.assert_almost_equal(offset_monthly["A"], rule_monthly["A"]) + + filled = rule_monthly.asfreq("B", method="pad") # noqa + # TODO: actually check that this worked. + + # don't forget! + filled_dep = rule_monthly.asfreq("B", method="pad") # noqa + + # test does not blow up on length-0 DataFrame + zero_length = datetime_frame.reindex([]) + result = zero_length.asfreq("BM") + assert result is not zero_length + + def test_asfreq_datetimeindex(self): + df = DataFrame( + {"A": [1, 2, 3]}, + index=[datetime(2011, 11, 1), datetime(2011, 11, 2), datetime(2011, 11, 3)], + ) + df = df.asfreq("B") + assert isinstance(df.index, DatetimeIndex) + + ts = df["A"].asfreq("B") + assert isinstance(ts.index, DatetimeIndex) + + def test_asfreq_fillvalue(self): + # test for fill value during upsampling, related to issue 3715 + + # setup + rng = date_range("1/1/2016", periods=10, freq="2S") + ts = Series(np.arange(len(rng)), index=rng) + df = DataFrame({"one": ts}) + + # insert pre-existing missing value + df.loc["2016-01-01 00:00:08", "one"] = None + + actual_df = df.asfreq(freq="1S", fill_value=9.0) + expected_df = df.asfreq(freq="1S").fillna(9.0) + expected_df.loc["2016-01-01 00:00:08", "one"] = None + tm.assert_frame_equal(expected_df, actual_df) + + expected_series = ts.asfreq(freq="1S").fillna(9.0) + actual_series = ts.asfreq(freq="1S", fill_value=9.0) + tm.assert_series_equal(expected_series, actual_series) diff --git a/pandas/tests/frame/methods/test_assign.py b/pandas/tests/frame/methods/test_assign.py new file mode 100644 index 0000000000000..63b9f031de188 --- /dev/null +++ b/pandas/tests/frame/methods/test_assign.py @@ -0,0 +1,82 @@ +import pytest + +from pandas import DataFrame +import pandas._testing as tm + + +class TestAssign: + def test_assign(self): + df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + original = df.copy() + result = df.assign(C=df.B / df.A) + expected = df.copy() + expected["C"] = [4, 2.5, 2] + tm.assert_frame_equal(result, expected) + + # lambda syntax + result = df.assign(C=lambda x: x.B / x.A) + tm.assert_frame_equal(result, expected) + + # original is unmodified + tm.assert_frame_equal(df, original) + + # Non-Series array-like + result = df.assign(C=[4, 2.5, 2]) + tm.assert_frame_equal(result, expected) + # original is unmodified + tm.assert_frame_equal(df, original) + + result = df.assign(B=df.B / df.A) + expected = expected.drop("B", axis=1).rename(columns={"C": "B"}) + tm.assert_frame_equal(result, expected) + + # overwrite + result = df.assign(A=df.A + df.B) + expected = df.copy() + expected["A"] = [5, 7, 9] + tm.assert_frame_equal(result, expected) + + # lambda + result = df.assign(A=lambda x: x.A + x.B) + tm.assert_frame_equal(result, expected) + + def test_assign_multiple(self): + df = DataFrame([[1, 4], [2, 5], [3, 6]], columns=["A", "B"]) + result = df.assign(C=[7, 8, 9], D=df.A, E=lambda x: x.B) + expected = DataFrame( + [[1, 4, 7, 1, 4], [2, 5, 8, 2, 5], [3, 6, 9, 3, 6]], columns=list("ABCDE") + ) + tm.assert_frame_equal(result, expected) + + def test_assign_order(self): + # GH 9818 + df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) + result = df.assign(D=df.A + df.B, C=df.A - df.B) + + expected = DataFrame([[1, 2, 3, -1], [3, 4, 7, -1]], columns=list("ABDC")) + tm.assert_frame_equal(result, expected) + result = df.assign(C=df.A - df.B, D=df.A + df.B) + + expected = DataFrame([[1, 2, -1, 3], [3, 4, -1, 7]], columns=list("ABCD")) + + tm.assert_frame_equal(result, expected) + + def test_assign_bad(self): + df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + + # non-keyword argument + with pytest.raises(TypeError): + df.assign(lambda x: x.A) + with pytest.raises(AttributeError): + df.assign(C=df.A, D=df.A + df.C) + + def test_assign_dependent(self): + df = DataFrame({"A": [1, 2], "B": [3, 4]}) + + result = df.assign(C=df.A, D=lambda x: x["A"] + x["C"]) + expected = DataFrame([[1, 3, 1, 2], [2, 4, 2, 4]], columns=list("ABCD")) + tm.assert_frame_equal(result, expected) + + result = df.assign(C=lambda df: df.A, D=lambda df: df["A"] + df["C"]) + expected = DataFrame([[1, 3, 1, 2], [2, 4, 2, 4]], columns=list("ABCD")) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_at_time.py b/pandas/tests/frame/methods/test_at_time.py new file mode 100644 index 0000000000000..108bbbfa183c4 --- /dev/null +++ b/pandas/tests/frame/methods/test_at_time.py @@ -0,0 +1,86 @@ +from datetime import time + +import numpy as np +import pytest +import pytz + +from pandas import DataFrame, date_range +import pandas._testing as tm + + +class TestAtTime: + def test_at_time(self): + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = DataFrame(np.random.randn(len(rng), 2), index=rng) + rs = ts.at_time(rng[1]) + assert (rs.index.hour == rng[1].hour).all() + assert (rs.index.minute == rng[1].minute).all() + assert (rs.index.second == rng[1].second).all() + + result = ts.at_time("9:30") + expected = ts.at_time(time(9, 30)) + tm.assert_frame_equal(result, expected) + + result = ts.loc[time(9, 30)] + expected = ts.loc[(rng.hour == 9) & (rng.minute == 30)] + + tm.assert_frame_equal(result, expected) + + # midnight, everything + rng = date_range("1/1/2000", "1/31/2000") + ts = DataFrame(np.random.randn(len(rng), 3), index=rng) + + result = ts.at_time(time(0, 0)) + tm.assert_frame_equal(result, ts) + + # time doesn't exist + rng = date_range("1/1/2012", freq="23Min", periods=384) + ts = DataFrame(np.random.randn(len(rng), 2), rng) + rs = ts.at_time("16:00") + assert len(rs) == 0 + + @pytest.mark.parametrize( + "hour", ["1:00", "1:00AM", time(1), time(1, tzinfo=pytz.UTC)] + ) + def test_at_time_errors(self, hour): + # GH#24043 + dti = date_range("2018", periods=3, freq="H") + df = DataFrame(list(range(len(dti))), index=dti) + if getattr(hour, "tzinfo", None) is None: + result = df.at_time(hour) + expected = df.iloc[1:2] + tm.assert_frame_equal(result, expected) + else: + with pytest.raises(ValueError, match="Index must be timezone"): + df.at_time(hour) + + def test_at_time_tz(self): + # GH#24043 + dti = date_range("2018", periods=3, freq="H", tz="US/Pacific") + df = DataFrame(list(range(len(dti))), index=dti) + result = df.at_time(time(4, tzinfo=pytz.timezone("US/Eastern"))) + expected = df.iloc[1:2] + tm.assert_frame_equal(result, expected) + + def test_at_time_raises(self): + # GH#20725 + df = DataFrame([[1, 2, 3], [4, 5, 6]]) + with pytest.raises(TypeError): # index is not a DatetimeIndex + df.at_time("00:00") + + @pytest.mark.parametrize("axis", ["index", "columns", 0, 1]) + def test_at_time_axis(self, axis): + # issue 8839 + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = DataFrame(np.random.randn(len(rng), len(rng))) + ts.index, ts.columns = rng, rng + + indices = rng[(rng.hour == 9) & (rng.minute == 30) & (rng.second == 0)] + + if axis in ["index", 0]: + expected = ts.loc[indices, :] + elif axis in ["columns", 1]: + expected = ts.loc[:, indices] + + result = ts.at_time("9:30", axis=axis) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_between_time.py b/pandas/tests/frame/methods/test_between_time.py new file mode 100644 index 0000000000000..b40604b4f4a16 --- /dev/null +++ b/pandas/tests/frame/methods/test_between_time.py @@ -0,0 +1,110 @@ +from datetime import time + +import numpy as np +import pytest + +from pandas import DataFrame, date_range +import pandas._testing as tm + + +class TestBetweenTime: + def test_between_time(self, close_open_fixture): + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = DataFrame(np.random.randn(len(rng), 2), index=rng) + stime = time(0, 0) + etime = time(1, 0) + inc_start, inc_end = close_open_fixture + + filtered = ts.between_time(stime, etime, inc_start, inc_end) + exp_len = 13 * 4 + 1 + if not inc_start: + exp_len -= 5 + if not inc_end: + exp_len -= 4 + + assert len(filtered) == exp_len + for rs in filtered.index: + t = rs.time() + if inc_start: + assert t >= stime + else: + assert t > stime + + if inc_end: + assert t <= etime + else: + assert t < etime + + result = ts.between_time("00:00", "01:00") + expected = ts.between_time(stime, etime) + tm.assert_frame_equal(result, expected) + + # across midnight + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = DataFrame(np.random.randn(len(rng), 2), index=rng) + stime = time(22, 0) + etime = time(9, 0) + + filtered = ts.between_time(stime, etime, inc_start, inc_end) + exp_len = (12 * 11 + 1) * 4 + 1 + if not inc_start: + exp_len -= 4 + if not inc_end: + exp_len -= 4 + + assert len(filtered) == exp_len + for rs in filtered.index: + t = rs.time() + if inc_start: + assert (t >= stime) or (t <= etime) + else: + assert (t > stime) or (t <= etime) + + if inc_end: + assert (t <= etime) or (t >= stime) + else: + assert (t < etime) or (t >= stime) + + def test_between_time_raises(self): + # GH#20725 + df = DataFrame([[1, 2, 3], [4, 5, 6]]) + with pytest.raises(TypeError): # index is not a DatetimeIndex + df.between_time(start_time="00:00", end_time="12:00") + + def test_between_time_axis(self, axis): + # GH#8839 + rng = date_range("1/1/2000", periods=100, freq="10min") + ts = DataFrame(np.random.randn(len(rng), len(rng))) + stime, etime = ("08:00:00", "09:00:00") + exp_len = 7 + + if axis in ["index", 0]: + ts.index = rng + assert len(ts.between_time(stime, etime)) == exp_len + assert len(ts.between_time(stime, etime, axis=0)) == exp_len + + if axis in ["columns", 1]: + ts.columns = rng + selected = ts.between_time(stime, etime, axis=1).columns + assert len(selected) == exp_len + + def test_between_time_axis_raises(self, axis): + # issue 8839 + rng = date_range("1/1/2000", periods=100, freq="10min") + mask = np.arange(0, len(rng)) + rand_data = np.random.randn(len(rng), len(rng)) + ts = DataFrame(rand_data, index=rng, columns=rng) + stime, etime = ("08:00:00", "09:00:00") + + msg = "Index must be DatetimeIndex" + if axis in ["columns", 1]: + ts.index = mask + with pytest.raises(TypeError, match=msg): + ts.between_time(stime, etime) + with pytest.raises(TypeError, match=msg): + ts.between_time(stime, etime, axis=0) + + if axis in ["index", 0]: + ts.columns = mask + with pytest.raises(TypeError, match=msg): + ts.between_time(stime, etime, axis=1) diff --git a/pandas/tests/frame/methods/test_combine.py b/pandas/tests/frame/methods/test_combine.py new file mode 100644 index 0000000000000..bc6a67e4e1f32 --- /dev/null +++ b/pandas/tests/frame/methods/test_combine.py @@ -0,0 +1,47 @@ +import numpy as np +import pytest + +import pandas as pd +import pandas._testing as tm + + +class TestCombine: + @pytest.mark.parametrize( + "data", + [ + pd.date_range("2000", periods=4), + pd.date_range("2000", periods=4, tz="US/Central"), + pd.period_range("2000", periods=4), + pd.timedelta_range(0, periods=4), + ], + ) + def test_combine_datetlike_udf(self, data): + # GH#23079 + df = pd.DataFrame({"A": data}) + other = df.copy() + df.iloc[1, 0] = None + + def combiner(a, b): + return b + + result = df.combine(other, combiner) + tm.assert_frame_equal(result, other) + + def test_combine_generic(self, float_frame): + df1 = float_frame + df2 = float_frame.loc[float_frame.index[:-5], ["A", "B", "C"]] + + combined = df1.combine(df2, np.add) + combined2 = df2.combine(df1, np.add) + assert combined["D"].isna().all() + assert combined2["D"].isna().all() + + chunk = combined.loc[combined.index[:-5], ["A", "B", "C"]] + chunk2 = combined2.loc[combined2.index[:-5], ["A", "B", "C"]] + + exp = ( + float_frame.loc[float_frame.index[:-5], ["A", "B", "C"]].reindex_like(chunk) + * 2 + ) + tm.assert_frame_equal(chunk, exp) + tm.assert_frame_equal(chunk2, exp) diff --git a/pandas/tests/frame/methods/test_describe.py b/pandas/tests/frame/methods/test_describe.py index 127233ed2713e..8a75e80a12f52 100644 --- a/pandas/tests/frame/methods/test_describe.py +++ b/pandas/tests/frame/methods/test_describe.py @@ -86,7 +86,7 @@ def test_describe_bool_frame(self): def test_describe_categorical(self): df = DataFrame({"value": np.random.randint(0, 10000, 100)}) - labels = ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] + labels = [f"{i} - {i + 499}" for i in range(0, 10000, 500)] cat_labels = Categorical(labels, labels) df = df.sort_values(by=["value"], ascending=True) diff --git a/pandas/tests/frame/methods/test_droplevel.py b/pandas/tests/frame/methods/test_droplevel.py new file mode 100644 index 0000000000000..517905cf23259 --- /dev/null +++ b/pandas/tests/frame/methods/test_droplevel.py @@ -0,0 +1,23 @@ +from pandas import DataFrame, Index, MultiIndex +import pandas._testing as tm + + +class TestDropLevel: + def test_droplevel(self): + # GH#20342 + df = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + df = df.set_index([0, 1]).rename_axis(["a", "b"]) + df.columns = MultiIndex.from_tuples( + [("c", "e"), ("d", "f")], names=["level_1", "level_2"] + ) + + # test that dropping of a level in index works + expected = df.reset_index("a", drop=True) + result = df.droplevel("a", axis="index") + tm.assert_frame_equal(result, expected) + + # test that dropping of a level in columns works + expected = df.copy() + expected.columns = Index(["c", "d"], name="level_1") + result = df.droplevel("level_2", axis="columns") + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_duplicated.py b/pandas/tests/frame/methods/test_duplicated.py index 72eec8753315c..38b9d7fd049ab 100644 --- a/pandas/tests/frame/methods/test_duplicated.py +++ b/pandas/tests/frame/methods/test_duplicated.py @@ -22,9 +22,7 @@ def test_duplicated_do_not_fail_on_wide_dataframes(): # gh-21524 # Given the wide dataframe with a lot of columns # with different (important!) values - data = { - "col_{0:02d}".format(i): np.random.randint(0, 1000, 30000) for i in range(100) - } + data = {f"col_{i:02d}": np.random.randint(0, 1000, 30000) for i in range(100)} df = DataFrame(data).T result = df.duplicated() diff --git a/pandas/tests/frame/methods/test_explode.py b/pandas/tests/frame/methods/test_explode.py index 76c87ed355492..bad8349ec977b 100644 --- a/pandas/tests/frame/methods/test_explode.py +++ b/pandas/tests/frame/methods/test_explode.py @@ -9,11 +9,11 @@ def test_error(): df = pd.DataFrame( {"A": pd.Series([[0, 1, 2], np.nan, [], (3, 4)], index=list("abcd")), "B": 1} ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="column must be a scalar"): df.explode(list("AA")) df.columns = list("AA") - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="columns must be unique"): df.explode("A") diff --git a/pandas/tests/frame/methods/test_first_and_last.py b/pandas/tests/frame/methods/test_first_and_last.py new file mode 100644 index 0000000000000..73e4128ddebb9 --- /dev/null +++ b/pandas/tests/frame/methods/test_first_and_last.py @@ -0,0 +1,61 @@ +""" +Note: includes tests for `last` +""" +import pytest + +from pandas import DataFrame +import pandas._testing as tm + + +class TestFirst: + def test_first_subset(self): + ts = tm.makeTimeDataFrame(freq="12h") + result = ts.first("10d") + assert len(result) == 20 + + ts = tm.makeTimeDataFrame(freq="D") + result = ts.first("10d") + assert len(result) == 10 + + result = ts.first("3M") + expected = ts[:"3/31/2000"] + tm.assert_frame_equal(result, expected) + + result = ts.first("21D") + expected = ts[:21] + tm.assert_frame_equal(result, expected) + + result = ts[:0].first("3M") + tm.assert_frame_equal(result, ts[:0]) + + def test_first_raises(self): + # GH#20725 + df = DataFrame([[1, 2, 3], [4, 5, 6]]) + with pytest.raises(TypeError): # index is not a DatetimeIndex + df.first("1D") + + def test_last_subset(self): + ts = tm.makeTimeDataFrame(freq="12h") + result = ts.last("10d") + assert len(result) == 20 + + ts = tm.makeTimeDataFrame(nper=30, freq="D") + result = ts.last("10d") + assert len(result) == 10 + + result = ts.last("21D") + expected = ts["2000-01-10":] + tm.assert_frame_equal(result, expected) + + result = ts.last("21D") + expected = ts[-21:] + tm.assert_frame_equal(result, expected) + + result = ts[:0].last("3M") + tm.assert_frame_equal(result, ts[:0]) + + def test_last_raises(self): + # GH20725 + df = DataFrame([[1, 2, 3], [4, 5, 6]]) + with pytest.raises(TypeError): # index is not a DatetimeIndex + df.last("1D") diff --git a/pandas/tests/frame/methods/test_interpolate.py b/pandas/tests/frame/methods/test_interpolate.py new file mode 100644 index 0000000000000..3b8fa0dfbb603 --- /dev/null +++ b/pandas/tests/frame/methods/test_interpolate.py @@ -0,0 +1,286 @@ +import numpy as np +import pytest + +import pandas.util._test_decorators as td + +from pandas import DataFrame, Series, date_range +import pandas._testing as tm + + +class TestDataFrameInterpolate: + def test_interp_basic(self): + df = DataFrame( + { + "A": [1, 2, np.nan, 4], + "B": [1, 4, 9, np.nan], + "C": [1, 2, 3, 5], + "D": list("abcd"), + } + ) + expected = DataFrame( + { + "A": [1.0, 2.0, 3.0, 4.0], + "B": [1.0, 4.0, 9.0, 9.0], + "C": [1, 2, 3, 5], + "D": list("abcd"), + } + ) + result = df.interpolate() + tm.assert_frame_equal(result, expected) + + result = df.set_index("C").interpolate() + expected = df.set_index("C") + expected.loc[3, "A"] = 3 + expected.loc[5, "B"] = 9 + tm.assert_frame_equal(result, expected) + + def test_interp_bad_method(self): + df = DataFrame( + { + "A": [1, 2, np.nan, 4], + "B": [1, 4, 9, np.nan], + "C": [1, 2, 3, 5], + "D": list("abcd"), + } + ) + with pytest.raises(ValueError): + df.interpolate(method="not_a_method") + + def test_interp_combo(self): + df = DataFrame( + { + "A": [1.0, 2.0, np.nan, 4.0], + "B": [1, 4, 9, np.nan], + "C": [1, 2, 3, 5], + "D": list("abcd"), + } + ) + + result = df["A"].interpolate() + expected = Series([1.0, 2.0, 3.0, 4.0], name="A") + tm.assert_series_equal(result, expected) + + result = df["A"].interpolate(downcast="infer") + expected = Series([1, 2, 3, 4], name="A") + tm.assert_series_equal(result, expected) + + def test_interp_nan_idx(self): + df = DataFrame({"A": [1, 2, np.nan, 4], "B": [np.nan, 2, 3, 4]}) + df = df.set_index("A") + with pytest.raises(NotImplementedError): + df.interpolate(method="values") + + @td.skip_if_no_scipy + def test_interp_various(self): + df = DataFrame( + {"A": [1, 2, np.nan, 4, 5, np.nan, 7], "C": [1, 2, 3, 5, 8, 13, 21]} + ) + df = df.set_index("C") + expected = df.copy() + result = df.interpolate(method="polynomial", order=1) + + expected.A.loc[3] = 2.66666667 + expected.A.loc[13] = 5.76923076 + tm.assert_frame_equal(result, expected) + + result = df.interpolate(method="cubic") + # GH #15662. + expected.A.loc[3] = 2.81547781 + expected.A.loc[13] = 5.52964175 + tm.assert_frame_equal(result, expected) + + result = df.interpolate(method="nearest") + expected.A.loc[3] = 2 + expected.A.loc[13] = 5 + tm.assert_frame_equal(result, expected, check_dtype=False) + + result = df.interpolate(method="quadratic") + expected.A.loc[3] = 2.82150771 + expected.A.loc[13] = 6.12648668 + tm.assert_frame_equal(result, expected) + + result = df.interpolate(method="slinear") + expected.A.loc[3] = 2.66666667 + expected.A.loc[13] = 5.76923077 + tm.assert_frame_equal(result, expected) + + result = df.interpolate(method="zero") + expected.A.loc[3] = 2.0 + expected.A.loc[13] = 5 + tm.assert_frame_equal(result, expected, check_dtype=False) + + @td.skip_if_no_scipy + def test_interp_alt_scipy(self): + df = DataFrame( + {"A": [1, 2, np.nan, 4, 5, np.nan, 7], "C": [1, 2, 3, 5, 8, 13, 21]} + ) + result = df.interpolate(method="barycentric") + expected = df.copy() + expected.loc[2, "A"] = 3 + expected.loc[5, "A"] = 6 + tm.assert_frame_equal(result, expected) + + result = df.interpolate(method="barycentric", downcast="infer") + tm.assert_frame_equal(result, expected.astype(np.int64)) + + result = df.interpolate(method="krogh") + expectedk = df.copy() + expectedk["A"] = expected["A"] + tm.assert_frame_equal(result, expectedk) + + result = df.interpolate(method="pchip") + expected.loc[2, "A"] = 3 + expected.loc[5, "A"] = 6.0 + + tm.assert_frame_equal(result, expected) + + def test_interp_rowwise(self): + df = DataFrame( + { + 0: [1, 2, np.nan, 4], + 1: [2, 3, 4, np.nan], + 2: [np.nan, 4, 5, 6], + 3: [4, np.nan, 6, 7], + 4: [1, 2, 3, 4], + } + ) + result = df.interpolate(axis=1) + expected = df.copy() + expected.loc[3, 1] = 5 + expected.loc[0, 2] = 3 + expected.loc[1, 3] = 3 + expected[4] = expected[4].astype(np.float64) + tm.assert_frame_equal(result, expected) + + result = df.interpolate(axis=1, method="values") + tm.assert_frame_equal(result, expected) + + result = df.interpolate(axis=0) + expected = df.interpolate() + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize( + "axis_name, axis_number", + [ + pytest.param("rows", 0, id="rows_0"), + pytest.param("index", 0, id="index_0"), + pytest.param("columns", 1, id="columns_1"), + ], + ) + def test_interp_axis_names(self, axis_name, axis_number): + # GH 29132: test axis names + data = {0: [0, np.nan, 6], 1: [1, np.nan, 7], 2: [2, 5, 8]} + + df = DataFrame(data, dtype=np.float64) + result = df.interpolate(axis=axis_name, method="linear") + expected = df.interpolate(axis=axis_number, method="linear") + tm.assert_frame_equal(result, expected) + + def test_rowwise_alt(self): + df = DataFrame( + { + 0: [0, 0.5, 1.0, np.nan, 4, 8, np.nan, np.nan, 64], + 1: [1, 2, 3, 4, 3, 2, 1, 0, -1], + } + ) + df.interpolate(axis=0) + # TODO: assert something? + + @pytest.mark.parametrize( + "check_scipy", [False, pytest.param(True, marks=td.skip_if_no_scipy)] + ) + def test_interp_leading_nans(self, check_scipy): + df = DataFrame( + {"A": [np.nan, np.nan, 0.5, 0.25, 0], "B": [np.nan, -3, -3.5, np.nan, -4]} + ) + result = df.interpolate() + expected = df.copy() + expected["B"].loc[3] = -3.75 + tm.assert_frame_equal(result, expected) + + if check_scipy: + result = df.interpolate(method="polynomial", order=1) + tm.assert_frame_equal(result, expected) + + def test_interp_raise_on_only_mixed(self): + df = DataFrame( + { + "A": [1, 2, np.nan, 4], + "B": ["a", "b", "c", "d"], + "C": [np.nan, 2, 5, 7], + "D": [np.nan, np.nan, 9, 9], + "E": [1, 2, 3, 4], + } + ) + with pytest.raises(TypeError): + df.interpolate(axis=1) + + def test_interp_raise_on_all_object_dtype(self): + # GH 22985 + df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, dtype="object") + msg = ( + "Cannot interpolate with all object-dtype columns " + "in the DataFrame. Try setting at least one " + "column to a numeric dtype." + ) + with pytest.raises(TypeError, match=msg): + df.interpolate() + + def test_interp_inplace(self): + df = DataFrame({"a": [1.0, 2.0, np.nan, 4.0]}) + expected = DataFrame({"a": [1.0, 2.0, 3.0, 4.0]}) + result = df.copy() + result["a"].interpolate(inplace=True) + tm.assert_frame_equal(result, expected) + + result = df.copy() + result["a"].interpolate(inplace=True, downcast="infer") + tm.assert_frame_equal(result, expected.astype("int64")) + + def test_interp_inplace_row(self): + # GH 10395 + result = DataFrame( + {"a": [1.0, 2.0, 3.0, 4.0], "b": [np.nan, 2.0, 3.0, 4.0], "c": [3, 2, 2, 2]} + ) + expected = result.interpolate(method="linear", axis=1, inplace=False) + result.interpolate(method="linear", axis=1, inplace=True) + tm.assert_frame_equal(result, expected) + + def test_interp_ignore_all_good(self): + # GH + df = DataFrame( + { + "A": [1, 2, np.nan, 4], + "B": [1, 2, 3, 4], + "C": [1.0, 2.0, np.nan, 4.0], + "D": [1.0, 2.0, 3.0, 4.0], + } + ) + expected = DataFrame( + { + "A": np.array([1, 2, 3, 4], dtype="float64"), + "B": np.array([1, 2, 3, 4], dtype="int64"), + "C": np.array([1.0, 2.0, 3, 4.0], dtype="float64"), + "D": np.array([1.0, 2.0, 3.0, 4.0], dtype="float64"), + } + ) + + result = df.interpolate(downcast=None) + tm.assert_frame_equal(result, expected) + + # all good + result = df[["B", "D"]].interpolate(downcast=None) + tm.assert_frame_equal(result, df[["B", "D"]]) + + @pytest.mark.parametrize("axis", [0, 1]) + def test_interp_time_inplace_axis(self, axis): + # GH 9687 + periods = 5 + idx = date_range(start="2014-01-01", periods=periods) + data = np.random.rand(periods, periods) + data[data < 0.5] = np.nan + expected = DataFrame(index=idx, columns=idx, data=data) + + result = expected.interpolate(axis=0, method="time") + expected.interpolate(axis=0, method="time", inplace=True) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_isin.py b/pandas/tests/frame/methods/test_isin.py index 0eb94afc99d94..6307738021f68 100644 --- a/pandas/tests/frame/methods/test_isin.py +++ b/pandas/tests/frame/methods/test_isin.py @@ -60,10 +60,14 @@ def test_isin_with_string_scalar(self): }, index=["foo", "bar", "baz", "qux"], ) - with pytest.raises(TypeError): + msg = ( + r"only list-like or dict-like objects are allowed " + r"to be passed to DataFrame.isin\(\), you passed a 'str'" + ) + with pytest.raises(TypeError, match=msg): df.isin("a") - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): df.isin("aaa") def test_isin_df(self): @@ -92,7 +96,8 @@ def test_isin_df_dupe_values(self): df1 = DataFrame({"A": [1, 2, 3, 4], "B": [2, np.nan, 4, 4]}) # just cols duped df2 = DataFrame([[0, 2], [12, 4], [2, np.nan], [4, 5]], columns=["B", "B"]) - with pytest.raises(ValueError): + msg = r"cannot compute isin with a duplicate axis\." + with pytest.raises(ValueError, match=msg): df1.isin(df2) # just index duped @@ -101,12 +106,12 @@ def test_isin_df_dupe_values(self): columns=["A", "B"], index=[0, 0, 1, 1], ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg): df1.isin(df2) # cols and index: df2.columns = ["B", "B"] - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=msg): df1.isin(df2) def test_isin_dupe_self(self): diff --git a/pandas/tests/frame/methods/test_quantile.py b/pandas/tests/frame/methods/test_quantile.py index 64461c08d34f4..9c52e8ec5620f 100644 --- a/pandas/tests/frame/methods/test_quantile.py +++ b/pandas/tests/frame/methods/test_quantile.py @@ -75,7 +75,8 @@ def test_quantile_axis_mixed(self): tm.assert_series_equal(result, expected) # must raise - with pytest.raises(TypeError): + msg = "'<' not supported between instances of 'Timestamp' and 'float'" + with pytest.raises(TypeError, match=msg): df.quantile(0.5, axis=1, numeric_only=False) def test_quantile_axis_parameter(self): diff --git a/pandas/tests/frame/methods/test_rename.py b/pandas/tests/frame/methods/test_rename.py new file mode 100644 index 0000000000000..e69a562f8214d --- /dev/null +++ b/pandas/tests/frame/methods/test_rename.py @@ -0,0 +1,353 @@ +from collections import ChainMap + +import numpy as np +import pytest + +from pandas import DataFrame, Index, MultiIndex +import pandas._testing as tm + + +class TestRename: + def test_rename(self, float_frame): + mapping = {"A": "a", "B": "b", "C": "c", "D": "d"} + + renamed = float_frame.rename(columns=mapping) + renamed2 = float_frame.rename(columns=str.lower) + + tm.assert_frame_equal(renamed, renamed2) + tm.assert_frame_equal( + renamed2.rename(columns=str.upper), float_frame, check_names=False + ) + + # index + data = {"A": {"foo": 0, "bar": 1}} + + # gets sorted alphabetical + df = DataFrame(data) + renamed = df.rename(index={"foo": "bar", "bar": "foo"}) + tm.assert_index_equal(renamed.index, Index(["foo", "bar"])) + + renamed = df.rename(index=str.upper) + tm.assert_index_equal(renamed.index, Index(["BAR", "FOO"])) + + # have to pass something + with pytest.raises(TypeError, match="must pass an index to rename"): + float_frame.rename() + + # partial columns + renamed = float_frame.rename(columns={"C": "foo", "D": "bar"}) + tm.assert_index_equal(renamed.columns, Index(["A", "B", "foo", "bar"])) + + # other axis + renamed = float_frame.T.rename(index={"C": "foo", "D": "bar"}) + tm.assert_index_equal(renamed.index, Index(["A", "B", "foo", "bar"])) + + # index with name + index = Index(["foo", "bar"], name="name") + renamer = DataFrame(data, index=index) + renamed = renamer.rename(index={"foo": "bar", "bar": "foo"}) + tm.assert_index_equal(renamed.index, Index(["bar", "foo"], name="name")) + assert renamed.index.name == renamer.index.name + + @pytest.mark.parametrize( + "args,kwargs", + [ + ((ChainMap({"A": "a"}, {"B": "b"}),), dict(axis="columns")), + ((), dict(columns=ChainMap({"A": "a"}, {"B": "b"}))), + ], + ) + def test_rename_chainmap(self, args, kwargs): + # see gh-23859 + colAData = range(1, 11) + colBdata = np.random.randn(10) + + df = DataFrame({"A": colAData, "B": colBdata}) + result = df.rename(*args, **kwargs) + + expected = DataFrame({"a": colAData, "b": colBdata}) + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize( + "kwargs, rename_index, rename_columns", + [ + ({"mapper": None, "axis": 0}, True, False), + ({"mapper": None, "axis": 1}, False, True), + ({"index": None}, True, False), + ({"columns": None}, False, True), + ({"index": None, "columns": None}, True, True), + ({}, False, False), + ], + ) + def test_rename_axis_none(self, kwargs, rename_index, rename_columns): + # GH 25034 + index = Index(list("abc"), name="foo") + columns = Index(["col1", "col2"], name="bar") + data = np.arange(6).reshape(3, 2) + df = DataFrame(data, index, columns) + + result = df.rename_axis(**kwargs) + expected_index = index.rename(None) if rename_index else index + expected_columns = columns.rename(None) if rename_columns else columns + expected = DataFrame(data, expected_index, expected_columns) + tm.assert_frame_equal(result, expected) + + def test_rename_multiindex(self): + + tuples_index = [("foo1", "bar1"), ("foo2", "bar2")] + tuples_columns = [("fizz1", "buzz1"), ("fizz2", "buzz2")] + index = MultiIndex.from_tuples(tuples_index, names=["foo", "bar"]) + columns = MultiIndex.from_tuples(tuples_columns, names=["fizz", "buzz"]) + df = DataFrame([(0, 0), (1, 1)], index=index, columns=columns) + + # + # without specifying level -> across all levels + + renamed = df.rename( + index={"foo1": "foo3", "bar2": "bar3"}, + columns={"fizz1": "fizz3", "buzz2": "buzz3"}, + ) + new_index = MultiIndex.from_tuples( + [("foo3", "bar1"), ("foo2", "bar3")], names=["foo", "bar"] + ) + new_columns = MultiIndex.from_tuples( + [("fizz3", "buzz1"), ("fizz2", "buzz3")], names=["fizz", "buzz"] + ) + tm.assert_index_equal(renamed.index, new_index) + tm.assert_index_equal(renamed.columns, new_columns) + assert renamed.index.names == df.index.names + assert renamed.columns.names == df.columns.names + + # + # with specifying a level (GH13766) + + # dict + new_columns = MultiIndex.from_tuples( + [("fizz3", "buzz1"), ("fizz2", "buzz2")], names=["fizz", "buzz"] + ) + renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level=0) + tm.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level="fizz") + tm.assert_index_equal(renamed.columns, new_columns) + + new_columns = MultiIndex.from_tuples( + [("fizz1", "buzz1"), ("fizz2", "buzz3")], names=["fizz", "buzz"] + ) + renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level=1) + tm.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level="buzz") + tm.assert_index_equal(renamed.columns, new_columns) + + # function + func = str.upper + new_columns = MultiIndex.from_tuples( + [("FIZZ1", "buzz1"), ("FIZZ2", "buzz2")], names=["fizz", "buzz"] + ) + renamed = df.rename(columns=func, level=0) + tm.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns=func, level="fizz") + tm.assert_index_equal(renamed.columns, new_columns) + + new_columns = MultiIndex.from_tuples( + [("fizz1", "BUZZ1"), ("fizz2", "BUZZ2")], names=["fizz", "buzz"] + ) + renamed = df.rename(columns=func, level=1) + tm.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns=func, level="buzz") + tm.assert_index_equal(renamed.columns, new_columns) + + # index + new_index = MultiIndex.from_tuples( + [("foo3", "bar1"), ("foo2", "bar2")], names=["foo", "bar"] + ) + renamed = df.rename(index={"foo1": "foo3", "bar2": "bar3"}, level=0) + tm.assert_index_equal(renamed.index, new_index) + + def test_rename_nocopy(self, float_frame): + renamed = float_frame.rename(columns={"C": "foo"}, copy=False) + renamed["foo"] = 1.0 + assert (float_frame["C"] == 1.0).all() + + def test_rename_inplace(self, float_frame): + float_frame.rename(columns={"C": "foo"}) + assert "C" in float_frame + assert "foo" not in float_frame + + c_id = id(float_frame["C"]) + float_frame = float_frame.copy() + float_frame.rename(columns={"C": "foo"}, inplace=True) + + assert "C" not in float_frame + assert "foo" in float_frame + assert id(float_frame["foo"]) != c_id + + def test_rename_bug(self): + # GH 5344 + # rename set ref_locs, and set_index was not resetting + df = DataFrame({0: ["foo", "bar"], 1: ["bah", "bas"], 2: [1, 2]}) + df = df.rename(columns={0: "a"}) + df = df.rename(columns={1: "b"}) + df = df.set_index(["a", "b"]) + df.columns = ["2001-01-01"] + expected = DataFrame( + [[1], [2]], + index=MultiIndex.from_tuples( + [("foo", "bah"), ("bar", "bas")], names=["a", "b"] + ), + columns=["2001-01-01"], + ) + tm.assert_frame_equal(df, expected) + + def test_rename_bug2(self): + # GH 19497 + # rename was changing Index to MultiIndex if Index contained tuples + + df = DataFrame(data=np.arange(3), index=[(0, 0), (1, 1), (2, 2)], columns=["a"]) + df = df.rename({(1, 1): (5, 4)}, axis="index") + expected = DataFrame( + data=np.arange(3), index=[(0, 0), (5, 4), (2, 2)], columns=["a"] + ) + tm.assert_frame_equal(df, expected) + + def test_rename_errors_raises(self): + df = DataFrame(columns=["A", "B", "C", "D"]) + with pytest.raises(KeyError, match="'E'] not found in axis"): + df.rename(columns={"A": "a", "E": "e"}, errors="raise") + + @pytest.mark.parametrize( + "mapper, errors, expected_columns", + [ + ({"A": "a", "E": "e"}, "ignore", ["a", "B", "C", "D"]), + ({"A": "a"}, "raise", ["a", "B", "C", "D"]), + (str.lower, "raise", ["a", "b", "c", "d"]), + ], + ) + def test_rename_errors(self, mapper, errors, expected_columns): + # GH 13473 + # rename now works with errors parameter + df = DataFrame(columns=["A", "B", "C", "D"]) + result = df.rename(columns=mapper, errors=errors) + expected = DataFrame(columns=expected_columns) + tm.assert_frame_equal(result, expected) + + def test_rename_objects(self, float_string_frame): + renamed = float_string_frame.rename(columns=str.upper) + + assert "FOO" in renamed + assert "foo" not in renamed + + def test_rename_axis_style(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["X", "Y"]) + expected = DataFrame({"a": [1, 2], "b": [1, 2]}, index=["X", "Y"]) + + result = df.rename(str.lower, axis=1) + tm.assert_frame_equal(result, expected) + + result = df.rename(str.lower, axis="columns") + tm.assert_frame_equal(result, expected) + + result = df.rename({"A": "a", "B": "b"}, axis=1) + tm.assert_frame_equal(result, expected) + + result = df.rename({"A": "a", "B": "b"}, axis="columns") + tm.assert_frame_equal(result, expected) + + # Index + expected = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["x", "y"]) + result = df.rename(str.lower, axis=0) + tm.assert_frame_equal(result, expected) + + result = df.rename(str.lower, axis="index") + tm.assert_frame_equal(result, expected) + + result = df.rename({"X": "x", "Y": "y"}, axis=0) + tm.assert_frame_equal(result, expected) + + result = df.rename({"X": "x", "Y": "y"}, axis="index") + tm.assert_frame_equal(result, expected) + + result = df.rename(mapper=str.lower, axis="index") + tm.assert_frame_equal(result, expected) + + def test_rename_mapper_multi(self): + df = DataFrame({"A": ["a", "b"], "B": ["c", "d"], "C": [1, 2]}).set_index( + ["A", "B"] + ) + result = df.rename(str.upper) + expected = df.rename(index=str.upper) + tm.assert_frame_equal(result, expected) + + def test_rename_positional_named(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = DataFrame({"a": [1, 2], "b": [1, 2]}, index=["X", "Y"]) + result = df.rename(index=str.lower, columns=str.upper) + expected = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["x", "y"]) + tm.assert_frame_equal(result, expected) + + def test_rename_axis_style_raises(self): + # see gh-12392 + df = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["0", "1"]) + + # Named target and axis + over_spec_msg = "Cannot specify both 'axis' and any of 'index' or 'columns'" + with pytest.raises(TypeError, match=over_spec_msg): + df.rename(index=str.lower, axis=1) + + with pytest.raises(TypeError, match=over_spec_msg): + df.rename(index=str.lower, axis="columns") + + with pytest.raises(TypeError, match=over_spec_msg): + df.rename(columns=str.lower, axis="columns") + + with pytest.raises(TypeError, match=over_spec_msg): + df.rename(index=str.lower, axis=0) + + # Multiple targets and axis + with pytest.raises(TypeError, match=over_spec_msg): + df.rename(str.lower, index=str.lower, axis="columns") + + # Too many targets + over_spec_msg = "Cannot specify both 'mapper' and any of 'index' or 'columns'" + with pytest.raises(TypeError, match=over_spec_msg): + df.rename(str.lower, index=str.lower, columns=str.lower) + + # Duplicates + with pytest.raises(TypeError, match="multiple values"): + df.rename(id, mapper=id) + + def test_rename_positional_raises(self): + # GH 29136 + df = DataFrame(columns=["A", "B"]) + msg = r"rename\(\) takes from 1 to 2 positional arguments" + + with pytest.raises(TypeError, match=msg): + df.rename(None, str.lower) + + def test_rename_no_mappings_raises(self): + # GH 29136 + df = DataFrame([[1]]) + msg = "must pass an index to rename" + with pytest.raises(TypeError, match=msg): + df.rename() + + with pytest.raises(TypeError, match=msg): + df.rename(None, index=None) + + with pytest.raises(TypeError, match=msg): + df.rename(None, columns=None) + + with pytest.raises(TypeError, match=msg): + df.rename(None, columns=None, index=None) + + def test_rename_mapper_and_positional_arguments_raises(self): + # GH 29136 + df = DataFrame([[1]]) + msg = "Cannot specify both 'mapper' and any of 'index' or 'columns'" + with pytest.raises(TypeError, match=msg): + df.rename({}, index={}) + + with pytest.raises(TypeError, match=msg): + df.rename({}, columns={}) + + with pytest.raises(TypeError, match=msg): + df.rename({}, columns={}, index={}) diff --git a/pandas/tests/frame/methods/test_reset_index.py b/pandas/tests/frame/methods/test_reset_index.py new file mode 100644 index 0000000000000..6586c19af2539 --- /dev/null +++ b/pandas/tests/frame/methods/test_reset_index.py @@ -0,0 +1,299 @@ +from datetime import datetime + +import numpy as np +import pytest + +from pandas import ( + DataFrame, + Index, + IntervalIndex, + MultiIndex, + RangeIndex, + Series, + Timestamp, + date_range, +) +import pandas._testing as tm + + +class TestResetIndex: + def test_reset_index_tz(self, tz_aware_fixture): + # GH 3950 + # reset_index with single level + tz = tz_aware_fixture + idx = date_range("1/1/2011", periods=5, freq="D", tz=tz, name="idx") + df = DataFrame({"a": range(5), "b": ["A", "B", "C", "D", "E"]}, index=idx) + + expected = DataFrame( + { + "idx": [ + datetime(2011, 1, 1), + datetime(2011, 1, 2), + datetime(2011, 1, 3), + datetime(2011, 1, 4), + datetime(2011, 1, 5), + ], + "a": range(5), + "b": ["A", "B", "C", "D", "E"], + }, + columns=["idx", "a", "b"], + ) + expected["idx"] = expected["idx"].apply(lambda d: Timestamp(d, tz=tz)) + tm.assert_frame_equal(df.reset_index(), expected) + + def test_reset_index_with_intervals(self): + idx = IntervalIndex.from_breaks(np.arange(11), name="x") + original = DataFrame({"x": idx, "y": np.arange(10)})[["x", "y"]] + + result = original.set_index("x") + expected = DataFrame({"y": np.arange(10)}, index=idx) + tm.assert_frame_equal(result, expected) + + result2 = result.reset_index() + tm.assert_frame_equal(result2, original) + + def test_reset_index(self, float_frame): + stacked = float_frame.stack()[::2] + stacked = DataFrame({"foo": stacked, "bar": stacked}) + + names = ["first", "second"] + stacked.index.names = names + deleveled = stacked.reset_index() + for i, (lev, level_codes) in enumerate( + zip(stacked.index.levels, stacked.index.codes) + ): + values = lev.take(level_codes) + name = names[i] + tm.assert_index_equal(values, Index(deleveled[name])) + + stacked.index.names = [None, None] + deleveled2 = stacked.reset_index() + tm.assert_series_equal( + deleveled["first"], deleveled2["level_0"], check_names=False + ) + tm.assert_series_equal( + deleveled["second"], deleveled2["level_1"], check_names=False + ) + + # default name assigned + rdf = float_frame.reset_index() + exp = Series(float_frame.index.values, name="index") + tm.assert_series_equal(rdf["index"], exp) + + # default name assigned, corner case + df = float_frame.copy() + df["index"] = "foo" + rdf = df.reset_index() + exp = Series(float_frame.index.values, name="level_0") + tm.assert_series_equal(rdf["level_0"], exp) + + # but this is ok + float_frame.index.name = "index" + deleveled = float_frame.reset_index() + tm.assert_series_equal(deleveled["index"], Series(float_frame.index)) + tm.assert_index_equal(deleveled.index, Index(np.arange(len(deleveled)))) + + # preserve column names + float_frame.columns.name = "columns" + resetted = float_frame.reset_index() + assert resetted.columns.name == "columns" + + # only remove certain columns + df = float_frame.reset_index().set_index(["index", "A", "B"]) + rs = df.reset_index(["A", "B"]) + + # TODO should reset_index check_names ? + tm.assert_frame_equal(rs, float_frame, check_names=False) + + rs = df.reset_index(["index", "A", "B"]) + tm.assert_frame_equal(rs, float_frame.reset_index(), check_names=False) + + rs = df.reset_index(["index", "A", "B"]) + tm.assert_frame_equal(rs, float_frame.reset_index(), check_names=False) + + rs = df.reset_index("A") + xp = float_frame.reset_index().set_index(["index", "B"]) + tm.assert_frame_equal(rs, xp, check_names=False) + + # test resetting in place + df = float_frame.copy() + resetted = float_frame.reset_index() + df.reset_index(inplace=True) + tm.assert_frame_equal(df, resetted, check_names=False) + + df = float_frame.reset_index().set_index(["index", "A", "B"]) + rs = df.reset_index("A", drop=True) + xp = float_frame.copy() + del xp["A"] + xp = xp.set_index(["B"], append=True) + tm.assert_frame_equal(rs, xp, check_names=False) + + def test_reset_index_name(self): + df = DataFrame( + [[1, 2, 3, 4], [5, 6, 7, 8]], + columns=["A", "B", "C", "D"], + index=Index(range(2), name="x"), + ) + assert df.reset_index().index.name is None + assert df.reset_index(drop=True).index.name is None + df.reset_index(inplace=True) + assert df.index.name is None + + def test_reset_index_level(self): + df = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]], columns=["A", "B", "C", "D"]) + + for levels in ["A", "B"], [0, 1]: + # With MultiIndex + result = df.set_index(["A", "B"]).reset_index(level=levels[0]) + tm.assert_frame_equal(result, df.set_index("B")) + + result = df.set_index(["A", "B"]).reset_index(level=levels[:1]) + tm.assert_frame_equal(result, df.set_index("B")) + + result = df.set_index(["A", "B"]).reset_index(level=levels) + tm.assert_frame_equal(result, df) + + result = df.set_index(["A", "B"]).reset_index(level=levels, drop=True) + tm.assert_frame_equal(result, df[["C", "D"]]) + + # With single-level Index (GH 16263) + result = df.set_index("A").reset_index(level=levels[0]) + tm.assert_frame_equal(result, df) + + result = df.set_index("A").reset_index(level=levels[:1]) + tm.assert_frame_equal(result, df) + + result = df.set_index(["A"]).reset_index(level=levels[0], drop=True) + tm.assert_frame_equal(result, df[["B", "C", "D"]]) + + # Missing levels - for both MultiIndex and single-level Index: + for idx_lev in ["A", "B"], ["A"]: + with pytest.raises(KeyError, match=r"(L|l)evel \(?E\)?"): + df.set_index(idx_lev).reset_index(level=["A", "E"]) + with pytest.raises(IndexError, match="Too many levels"): + df.set_index(idx_lev).reset_index(level=[0, 1, 2]) + + def test_reset_index_right_dtype(self): + time = np.arange(0.0, 10, np.sqrt(2) / 2) + s1 = Series( + (9.81 * time ** 2) / 2, index=Index(time, name="time"), name="speed" + ) + df = DataFrame(s1) + + resetted = s1.reset_index() + assert resetted["time"].dtype == np.float64 + + resetted = df.reset_index() + assert resetted["time"].dtype == np.float64 + + def test_reset_index_multiindex_col(self): + vals = np.random.randn(3, 3).astype(object) + idx = ["x", "y", "z"] + full = np.hstack(([[x] for x in idx], vals)) + df = DataFrame( + vals, + Index(idx, name="a"), + columns=[["b", "b", "c"], ["mean", "median", "mean"]], + ) + rs = df.reset_index() + xp = DataFrame( + full, columns=[["a", "b", "b", "c"], ["", "mean", "median", "mean"]] + ) + tm.assert_frame_equal(rs, xp) + + rs = df.reset_index(col_fill=None) + xp = DataFrame( + full, columns=[["a", "b", "b", "c"], ["a", "mean", "median", "mean"]] + ) + tm.assert_frame_equal(rs, xp) + + rs = df.reset_index(col_level=1, col_fill="blah") + xp = DataFrame( + full, columns=[["blah", "b", "b", "c"], ["a", "mean", "median", "mean"]] + ) + tm.assert_frame_equal(rs, xp) + + df = DataFrame( + vals, + MultiIndex.from_arrays([[0, 1, 2], ["x", "y", "z"]], names=["d", "a"]), + columns=[["b", "b", "c"], ["mean", "median", "mean"]], + ) + rs = df.reset_index("a") + xp = DataFrame( + full, + Index([0, 1, 2], name="d"), + columns=[["a", "b", "b", "c"], ["", "mean", "median", "mean"]], + ) + tm.assert_frame_equal(rs, xp) + + rs = df.reset_index("a", col_fill=None) + xp = DataFrame( + full, + Index(range(3), name="d"), + columns=[["a", "b", "b", "c"], ["a", "mean", "median", "mean"]], + ) + tm.assert_frame_equal(rs, xp) + + rs = df.reset_index("a", col_fill="blah", col_level=1) + xp = DataFrame( + full, + Index(range(3), name="d"), + columns=[["blah", "b", "b", "c"], ["a", "mean", "median", "mean"]], + ) + tm.assert_frame_equal(rs, xp) + + def test_reset_index_multiindex_nan(self): + # GH#6322, testing reset_index on MultiIndexes + # when we have a nan or all nan + df = DataFrame( + {"A": ["a", "b", "c"], "B": [0, 1, np.nan], "C": np.random.rand(3)} + ) + rs = df.set_index(["A", "B"]).reset_index() + tm.assert_frame_equal(rs, df) + + df = DataFrame( + {"A": [np.nan, "b", "c"], "B": [0, 1, 2], "C": np.random.rand(3)} + ) + rs = df.set_index(["A", "B"]).reset_index() + tm.assert_frame_equal(rs, df) + + df = DataFrame({"A": ["a", "b", "c"], "B": [0, 1, 2], "C": [np.nan, 1.1, 2.2]}) + rs = df.set_index(["A", "B"]).reset_index() + tm.assert_frame_equal(rs, df) + + df = DataFrame( + { + "A": ["a", "b", "c"], + "B": [np.nan, np.nan, np.nan], + "C": np.random.rand(3), + } + ) + rs = df.set_index(["A", "B"]).reset_index() + tm.assert_frame_equal(rs, df) + + def test_reset_index_with_datetimeindex_cols(self): + # GH#5818 + df = DataFrame( + [[1, 2], [3, 4]], + columns=date_range("1/1/2013", "1/2/2013"), + index=["A", "B"], + ) + + result = df.reset_index() + expected = DataFrame( + [["A", 1, 2], ["B", 3, 4]], + columns=["index", datetime(2013, 1, 1), datetime(2013, 1, 2)], + ) + tm.assert_frame_equal(result, expected) + + def test_reset_index_range(self): + # GH#12071 + df = DataFrame([[0, 0], [1, 1]], columns=["A", "B"], index=RangeIndex(stop=2)) + result = df.reset_index() + assert isinstance(result.index, RangeIndex) + expected = DataFrame( + [[0, 0, 0], [1, 1, 1]], + columns=["index", "A", "B"], + index=RangeIndex(stop=2), + ) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_round.py b/pandas/tests/frame/methods/test_round.py index 0865e03cedc50..6dcdf49e93729 100644 --- a/pandas/tests/frame/methods/test_round.py +++ b/pandas/tests/frame/methods/test_round.py @@ -34,7 +34,8 @@ def test_round(self): # Round with a list round_list = [1, 2] - with pytest.raises(TypeError): + msg = "decimals must be an integer, a dict-like or a Series" + with pytest.raises(TypeError, match=msg): df.round(round_list) # Round with a dictionary @@ -57,34 +58,37 @@ def test_round(self): # float input to `decimals` non_int_round_dict = {"col1": 1, "col2": 0.5} - with pytest.raises(TypeError): + msg = "integer argument expected, got float" + with pytest.raises(TypeError, match=msg): df.round(non_int_round_dict) # String input non_int_round_dict = {"col1": 1, "col2": "foo"} - with pytest.raises(TypeError): + msg = r"an integer is required \(got type str\)" + with pytest.raises(TypeError, match=msg): df.round(non_int_round_dict) non_int_round_Series = Series(non_int_round_dict) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): df.round(non_int_round_Series) # List input non_int_round_dict = {"col1": 1, "col2": [1, 2]} - with pytest.raises(TypeError): + msg = r"an integer is required \(got type list\)" + with pytest.raises(TypeError, match=msg): df.round(non_int_round_dict) non_int_round_Series = Series(non_int_round_dict) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): df.round(non_int_round_Series) # Non integer Series inputs non_int_round_Series = Series(non_int_round_dict) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): df.round(non_int_round_Series) non_int_round_Series = Series(non_int_round_dict) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=msg): df.round(non_int_round_Series) # Negative numbers @@ -103,7 +107,8 @@ def test_round(self): {"col1": [1.123, 2.123, 3.123], "col2": [1.2, 2.2, 3.2]} ) - with pytest.raises(TypeError): + msg = "integer argument expected, got float" + with pytest.raises(TypeError, match=msg): df.round(nan_round_Series) # Make sure this doesn't break existing Series.round diff --git a/pandas/tests/frame/methods/test_select_dtypes.py b/pandas/tests/frame/methods/test_select_dtypes.py new file mode 100644 index 0000000000000..fe7baebcf0cf7 --- /dev/null +++ b/pandas/tests/frame/methods/test_select_dtypes.py @@ -0,0 +1,329 @@ +from collections import OrderedDict + +import numpy as np +import pytest + +import pandas as pd +from pandas import DataFrame, Timestamp +import pandas._testing as tm + + +class TestSelectDtypes: + def test_select_dtypes_include_using_list_like(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.Categorical(list("abc")), + "g": pd.date_range("20130101", periods=3), + "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), + "i": pd.date_range("20130101", periods=3, tz="CET"), + "j": pd.period_range("2013-01", periods=3, freq="M"), + "k": pd.timedelta_range("1 day", periods=3), + } + ) + + ri = df.select_dtypes(include=[np.number]) + ei = df[["b", "c", "d", "k"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include=[np.number], exclude=["timedelta"]) + ei = df[["b", "c", "d"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include=[np.number, "category"], exclude=["timedelta"]) + ei = df[["b", "c", "d", "f"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include=["datetime"]) + ei = df[["g"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include=["datetime64"]) + ei = df[["g"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include=["datetimetz"]) + ei = df[["h", "i"]] + tm.assert_frame_equal(ri, ei) + + with pytest.raises(NotImplementedError, match=r"^$"): + df.select_dtypes(include=["period"]) + + def test_select_dtypes_exclude_using_list_like(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + } + ) + re = df.select_dtypes(exclude=[np.number]) + ee = df[["a", "e"]] + tm.assert_frame_equal(re, ee) + + def test_select_dtypes_exclude_include_using_list_like(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.date_range("now", periods=3).values, + } + ) + exclude = (np.datetime64,) + include = np.bool_, "integer" + r = df.select_dtypes(include=include, exclude=exclude) + e = df[["b", "c", "e"]] + tm.assert_frame_equal(r, e) + + exclude = ("datetime",) + include = "bool", "int64", "int32" + r = df.select_dtypes(include=include, exclude=exclude) + e = df[["b", "e"]] + tm.assert_frame_equal(r, e) + + def test_select_dtypes_include_using_scalars(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.Categorical(list("abc")), + "g": pd.date_range("20130101", periods=3), + "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), + "i": pd.date_range("20130101", periods=3, tz="CET"), + "j": pd.period_range("2013-01", periods=3, freq="M"), + "k": pd.timedelta_range("1 day", periods=3), + } + ) + + ri = df.select_dtypes(include=np.number) + ei = df[["b", "c", "d", "k"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include="datetime") + ei = df[["g"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include="datetime64") + ei = df[["g"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include="category") + ei = df[["f"]] + tm.assert_frame_equal(ri, ei) + + with pytest.raises(NotImplementedError, match=r"^$"): + df.select_dtypes(include="period") + + def test_select_dtypes_exclude_using_scalars(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.Categorical(list("abc")), + "g": pd.date_range("20130101", periods=3), + "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), + "i": pd.date_range("20130101", periods=3, tz="CET"), + "j": pd.period_range("2013-01", periods=3, freq="M"), + "k": pd.timedelta_range("1 day", periods=3), + } + ) + + ri = df.select_dtypes(exclude=np.number) + ei = df[["a", "e", "f", "g", "h", "i", "j"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(exclude="category") + ei = df[["a", "b", "c", "d", "e", "g", "h", "i", "j", "k"]] + tm.assert_frame_equal(ri, ei) + + with pytest.raises(NotImplementedError, match=r"^$"): + df.select_dtypes(exclude="period") + + def test_select_dtypes_include_exclude_using_scalars(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.Categorical(list("abc")), + "g": pd.date_range("20130101", periods=3), + "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), + "i": pd.date_range("20130101", periods=3, tz="CET"), + "j": pd.period_range("2013-01", periods=3, freq="M"), + "k": pd.timedelta_range("1 day", periods=3), + } + ) + + ri = df.select_dtypes(include=np.number, exclude="floating") + ei = df[["b", "c", "k"]] + tm.assert_frame_equal(ri, ei) + + def test_select_dtypes_include_exclude_mixed_scalars_lists(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.Categorical(list("abc")), + "g": pd.date_range("20130101", periods=3), + "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), + "i": pd.date_range("20130101", periods=3, tz="CET"), + "j": pd.period_range("2013-01", periods=3, freq="M"), + "k": pd.timedelta_range("1 day", periods=3), + } + ) + + ri = df.select_dtypes(include=np.number, exclude=["floating", "timedelta"]) + ei = df[["b", "c"]] + tm.assert_frame_equal(ri, ei) + + ri = df.select_dtypes(include=[np.number, "category"], exclude="floating") + ei = df[["b", "c", "f", "k"]] + tm.assert_frame_equal(ri, ei) + + def test_select_dtypes_duplicate_columns(self): + # GH20839 + odict = OrderedDict + df = DataFrame( + odict( + [ + ("a", list("abc")), + ("b", list(range(1, 4))), + ("c", np.arange(3, 6).astype("u1")), + ("d", np.arange(4.0, 7.0, dtype="float64")), + ("e", [True, False, True]), + ("f", pd.date_range("now", periods=3).values), + ] + ) + ) + df.columns = ["a", "a", "b", "b", "b", "c"] + + expected = DataFrame( + {"a": list(range(1, 4)), "b": np.arange(3, 6).astype("u1")} + ) + + result = df.select_dtypes(include=[np.number], exclude=["floating"]) + tm.assert_frame_equal(result, expected) + + def test_select_dtypes_not_an_attr_but_still_valid_dtype(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.date_range("now", periods=3).values, + } + ) + df["g"] = df.f.diff() + assert not hasattr(np, "u8") + r = df.select_dtypes(include=["i8", "O"], exclude=["timedelta"]) + e = df[["a", "b"]] + tm.assert_frame_equal(r, e) + + r = df.select_dtypes(include=["i8", "O", "timedelta64[ns]"]) + e = df[["a", "b", "g"]] + tm.assert_frame_equal(r, e) + + def test_select_dtypes_empty(self): + df = DataFrame({"a": list("abc"), "b": list(range(1, 4))}) + msg = "at least one of include or exclude must be nonempty" + with pytest.raises(ValueError, match=msg): + df.select_dtypes() + + def test_select_dtypes_bad_datetime64(self): + df = DataFrame( + { + "a": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.date_range("now", periods=3).values, + } + ) + with pytest.raises(ValueError, match=".+ is too specific"): + df.select_dtypes(include=["datetime64[D]"]) + + with pytest.raises(ValueError, match=".+ is too specific"): + df.select_dtypes(exclude=["datetime64[as]"]) + + def test_select_dtypes_datetime_with_tz(self): + + df2 = DataFrame( + dict( + A=Timestamp("20130102", tz="US/Eastern"), + B=Timestamp("20130603", tz="CET"), + ), + index=range(5), + ) + df3 = pd.concat([df2.A.to_frame(), df2.B.to_frame()], axis=1) + result = df3.select_dtypes(include=["datetime64[ns]"]) + expected = df3.reindex(columns=[]) + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize( + "dtype", [str, "str", np.string_, "S1", "unicode", np.unicode_, "U1"] + ) + @pytest.mark.parametrize("arg", ["include", "exclude"]) + def test_select_dtypes_str_raises(self, dtype, arg): + df = DataFrame( + { + "a": list("abc"), + "g": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.date_range("now", periods=3).values, + } + ) + msg = "string dtypes are not allowed" + kwargs = {arg: [dtype]} + + with pytest.raises(TypeError, match=msg): + df.select_dtypes(**kwargs) + + def test_select_dtypes_bad_arg_raises(self): + df = DataFrame( + { + "a": list("abc"), + "g": list("abc"), + "b": list(range(1, 4)), + "c": np.arange(3, 6).astype("u1"), + "d": np.arange(4.0, 7.0, dtype="float64"), + "e": [True, False, True], + "f": pd.date_range("now", periods=3).values, + } + ) + + msg = "data type.*not understood" + with pytest.raises(TypeError, match=msg): + df.select_dtypes(["blargy, blarg, blarg"]) + + def test_select_dtypes_typecodes(self): + # GH 11990 + df = tm.makeCustomDataframe(30, 3, data_gen_f=lambda x, y: np.random.random()) + expected = df + FLOAT_TYPES = list(np.typecodes["AllFloat"]) + tm.assert_frame_equal(df.select_dtypes(FLOAT_TYPES), expected) diff --git a/pandas/tests/frame/methods/test_sort_values.py b/pandas/tests/frame/methods/test_sort_values.py index 96f4d6ed90d6b..5a25d1c2c0894 100644 --- a/pandas/tests/frame/methods/test_sort_values.py +++ b/pandas/tests/frame/methods/test_sort_values.py @@ -458,7 +458,7 @@ def test_sort_values_na_position_with_categories_raises(self): } ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="invalid na_position: bad_position"): df.sort_values(by="c", ascending=False, na_position="bad_position") @pytest.mark.parametrize("inplace", [True, False]) diff --git a/pandas/tests/frame/methods/test_to_dict.py b/pandas/tests/frame/methods/test_to_dict.py index 7b0adceb57668..cd9bd169322fd 100644 --- a/pandas/tests/frame/methods/test_to_dict.py +++ b/pandas/tests/frame/methods/test_to_dict.py @@ -132,7 +132,13 @@ def test_to_dict(self, mapping): def test_to_dict_errors(self, mapping): # GH#16122 df = DataFrame(np.random.randn(3, 3)) - with pytest.raises(TypeError): + msg = "|".join( + [ + "unsupported type: ", + r"to_dict\(\) only accepts initialized defaultdicts", + ] + ) + with pytest.raises(TypeError, match=msg): df.to_dict(into=mapping) def test_to_dict_not_unique_warning(self): @@ -236,9 +242,9 @@ def test_to_dict_numeric_names(self): def test_to_dict_wide(self): # GH#24939 - df = DataFrame({("A_{:d}".format(i)): [i] for i in range(256)}) + df = DataFrame({(f"A_{i:d}"): [i] for i in range(256)}) result = df.to_dict("records")[0] - expected = {"A_{:d}".format(i): i for i in range(256)} + expected = {f"A_{i:d}": i for i in range(256)} assert result == expected def test_to_dict_orient_dtype(self): diff --git a/pandas/tests/frame/methods/test_to_period.py b/pandas/tests/frame/methods/test_to_period.py new file mode 100644 index 0000000000000..eac78e611b008 --- /dev/null +++ b/pandas/tests/frame/methods/test_to_period.py @@ -0,0 +1,36 @@ +import numpy as np +import pytest + +from pandas import DataFrame, date_range, period_range +import pandas._testing as tm + + +class TestToPeriod: + def test_frame_to_period(self): + K = 5 + + dr = date_range("1/1/2000", "1/1/2001") + pr = period_range("1/1/2000", "1/1/2001") + df = DataFrame(np.random.randn(len(dr), K), index=dr) + df["mix"] = "a" + + pts = df.to_period() + exp = df.copy() + exp.index = pr + tm.assert_frame_equal(pts, exp) + + pts = df.to_period("M") + tm.assert_index_equal(pts.index, exp.index.asfreq("M")) + + df = df.T + pts = df.to_period(axis=1) + exp = df.copy() + exp.columns = pr + tm.assert_frame_equal(pts, exp) + + pts = df.to_period("M", axis=1) + tm.assert_index_equal(pts.columns, exp.columns.asfreq("M")) + + msg = "No axis named 2 for object type " + with pytest.raises(ValueError, match=msg): + df.to_period(axis=2) diff --git a/pandas/tests/frame/methods/test_to_timestamp.py b/pandas/tests/frame/methods/test_to_timestamp.py new file mode 100644 index 0000000000000..ae7d2827e05a6 --- /dev/null +++ b/pandas/tests/frame/methods/test_to_timestamp.py @@ -0,0 +1,103 @@ +from datetime import timedelta + +import numpy as np +import pytest + +from pandas import ( + DataFrame, + DatetimeIndex, + Timedelta, + date_range, + period_range, + to_datetime, +) +import pandas._testing as tm + + +class TestToTimestamp: + def test_frame_to_time_stamp(self): + K = 5 + index = period_range(freq="A", start="1/1/2001", end="12/1/2009") + df = DataFrame(np.random.randn(len(index), K), index=index) + df["mix"] = "a" + + exp_index = date_range("1/1/2001", end="12/31/2009", freq="A-DEC") + exp_index = exp_index + Timedelta(1, "D") - Timedelta(1, "ns") + result = df.to_timestamp("D", "end") + tm.assert_index_equal(result.index, exp_index) + tm.assert_numpy_array_equal(result.values, df.values) + + exp_index = date_range("1/1/2001", end="1/1/2009", freq="AS-JAN") + result = df.to_timestamp("D", "start") + tm.assert_index_equal(result.index, exp_index) + + def _get_with_delta(delta, freq="A-DEC"): + return date_range( + to_datetime("1/1/2001") + delta, + to_datetime("12/31/2009") + delta, + freq=freq, + ) + + delta = timedelta(hours=23) + result = df.to_timestamp("H", "end") + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "h") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + + delta = timedelta(hours=23, minutes=59) + result = df.to_timestamp("T", "end") + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "m") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + + result = df.to_timestamp("S", "end") + delta = timedelta(hours=23, minutes=59, seconds=59) + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "s") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + + # columns + df = df.T + + exp_index = date_range("1/1/2001", end="12/31/2009", freq="A-DEC") + exp_index = exp_index + Timedelta(1, "D") - Timedelta(1, "ns") + result = df.to_timestamp("D", "end", axis=1) + tm.assert_index_equal(result.columns, exp_index) + tm.assert_numpy_array_equal(result.values, df.values) + + exp_index = date_range("1/1/2001", end="1/1/2009", freq="AS-JAN") + result = df.to_timestamp("D", "start", axis=1) + tm.assert_index_equal(result.columns, exp_index) + + delta = timedelta(hours=23) + result = df.to_timestamp("H", "end", axis=1) + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "h") - Timedelta(1, "ns") + tm.assert_index_equal(result.columns, exp_index) + + delta = timedelta(hours=23, minutes=59) + result = df.to_timestamp("T", "end", axis=1) + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "m") - Timedelta(1, "ns") + tm.assert_index_equal(result.columns, exp_index) + + result = df.to_timestamp("S", "end", axis=1) + delta = timedelta(hours=23, minutes=59, seconds=59) + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "s") - Timedelta(1, "ns") + tm.assert_index_equal(result.columns, exp_index) + + # invalid axis + with pytest.raises(ValueError, match="axis"): + df.to_timestamp(axis=2) + + result1 = df.to_timestamp("5t", axis=1) + result2 = df.to_timestamp("t", axis=1) + expected = date_range("2001-01-01", "2009-01-01", freq="AS") + assert isinstance(result1.columns, DatetimeIndex) + assert isinstance(result2.columns, DatetimeIndex) + tm.assert_numpy_array_equal(result1.columns.asi8, expected.asi8) + tm.assert_numpy_array_equal(result2.columns.asi8, expected.asi8) + # PeriodIndex.to_timestamp always use 'infer' + assert result1.columns.freqstr == "AS-JAN" + assert result2.columns.freqstr == "AS-JAN" diff --git a/pandas/tests/frame/methods/test_tz_convert.py b/pandas/tests/frame/methods/test_tz_convert.py new file mode 100644 index 0000000000000..ea8c4b88538d4 --- /dev/null +++ b/pandas/tests/frame/methods/test_tz_convert.py @@ -0,0 +1,84 @@ +import numpy as np +import pytest + +from pandas import DataFrame, Index, MultiIndex, date_range +import pandas._testing as tm + + +class TestTZConvert: + def test_frame_tz_convert(self): + rng = date_range("1/1/2011", periods=200, freq="D", tz="US/Eastern") + + df = DataFrame({"a": 1}, index=rng) + result = df.tz_convert("Europe/Berlin") + expected = DataFrame({"a": 1}, rng.tz_convert("Europe/Berlin")) + assert result.index.tz.zone == "Europe/Berlin" + tm.assert_frame_equal(result, expected) + + df = df.T + result = df.tz_convert("Europe/Berlin", axis=1) + assert result.columns.tz.zone == "Europe/Berlin" + tm.assert_frame_equal(result, expected.T) + + @pytest.mark.parametrize("fn", ["tz_localize", "tz_convert"]) + def test_tz_convert_and_localize(self, fn): + l0 = date_range("20140701", periods=5, freq="D") + l1 = date_range("20140701", periods=5, freq="D") + + int_idx = Index(range(5)) + + if fn == "tz_convert": + l0 = l0.tz_localize("UTC") + l1 = l1.tz_localize("UTC") + + for idx in [l0, l1]: + + l0_expected = getattr(idx, fn)("US/Pacific") + l1_expected = getattr(idx, fn)("US/Pacific") + + df1 = DataFrame(np.ones(5), index=l0) + df1 = getattr(df1, fn)("US/Pacific") + tm.assert_index_equal(df1.index, l0_expected) + + # MultiIndex + # GH7846 + df2 = DataFrame(np.ones(5), MultiIndex.from_arrays([l0, l1])) + + df3 = getattr(df2, fn)("US/Pacific", level=0) + assert not df3.index.levels[0].equals(l0) + tm.assert_index_equal(df3.index.levels[0], l0_expected) + tm.assert_index_equal(df3.index.levels[1], l1) + assert not df3.index.levels[1].equals(l1_expected) + + df3 = getattr(df2, fn)("US/Pacific", level=1) + tm.assert_index_equal(df3.index.levels[0], l0) + assert not df3.index.levels[0].equals(l0_expected) + tm.assert_index_equal(df3.index.levels[1], l1_expected) + assert not df3.index.levels[1].equals(l1) + + df4 = DataFrame(np.ones(5), MultiIndex.from_arrays([int_idx, l0])) + + # TODO: untested + df5 = getattr(df4, fn)("US/Pacific", level=1) # noqa + + tm.assert_index_equal(df3.index.levels[0], l0) + assert not df3.index.levels[0].equals(l0_expected) + tm.assert_index_equal(df3.index.levels[1], l1_expected) + assert not df3.index.levels[1].equals(l1) + + # Bad Inputs + + # Not DatetimeIndex / PeriodIndex + with pytest.raises(TypeError, match="DatetimeIndex"): + df = DataFrame(index=int_idx) + df = getattr(df, fn)("US/Pacific") + + # Not DatetimeIndex / PeriodIndex + with pytest.raises(TypeError, match="DatetimeIndex"): + df = DataFrame(np.ones(5), MultiIndex.from_arrays([int_idx, l0])) + df = getattr(df, fn)("US/Pacific", level=0) + + # Invalid level + with pytest.raises(ValueError, match="not valid"): + df = DataFrame(index=l0) + df = getattr(df, fn)("US/Pacific", level=1) diff --git a/pandas/tests/frame/methods/test_tz_localize.py b/pandas/tests/frame/methods/test_tz_localize.py new file mode 100644 index 0000000000000..1d4e26a6999b7 --- /dev/null +++ b/pandas/tests/frame/methods/test_tz_localize.py @@ -0,0 +1,21 @@ +from pandas import DataFrame, date_range +import pandas._testing as tm + + +class TestTZLocalize: + # See also: + # test_tz_convert_and_localize in test_tz_convert + + def test_frame_tz_localize(self): + rng = date_range("1/1/2011", periods=100, freq="H") + + df = DataFrame({"a": 1}, index=rng) + result = df.tz_localize("utc") + expected = DataFrame({"a": 1}, rng.tz_localize("UTC")) + assert result.index.tz.zone == "UTC" + tm.assert_frame_equal(result, expected) + + df = df.T + result = df.tz_localize("utc", axis=1) + assert result.columns.tz.zone == "UTC" + tm.assert_frame_equal(result, expected.T) diff --git a/pandas/tests/frame/methods/test_value_counts.py b/pandas/tests/frame/methods/test_value_counts.py new file mode 100644 index 0000000000000..c409b0bbe6fa9 --- /dev/null +++ b/pandas/tests/frame/methods/test_value_counts.py @@ -0,0 +1,102 @@ +import numpy as np + +import pandas as pd +import pandas._testing as tm + + +def test_data_frame_value_counts_unsorted(): + df = pd.DataFrame( + {"num_legs": [2, 4, 4, 6], "num_wings": [2, 0, 0, 0]}, + index=["falcon", "dog", "cat", "ant"], + ) + + result = df.value_counts(sort=False) + expected = pd.Series( + data=[1, 2, 1], + index=pd.MultiIndex.from_arrays( + [(2, 4, 6), (2, 0, 0)], names=["num_legs", "num_wings"] + ), + ) + + tm.assert_series_equal(result, expected) + + +def test_data_frame_value_counts_ascending(): + df = pd.DataFrame( + {"num_legs": [2, 4, 4, 6], "num_wings": [2, 0, 0, 0]}, + index=["falcon", "dog", "cat", "ant"], + ) + + result = df.value_counts(ascending=True) + expected = pd.Series( + data=[1, 1, 2], + index=pd.MultiIndex.from_arrays( + [(2, 6, 4), (2, 0, 0)], names=["num_legs", "num_wings"] + ), + ) + + tm.assert_series_equal(result, expected) + + +def test_data_frame_value_counts_default(): + df = pd.DataFrame( + {"num_legs": [2, 4, 4, 6], "num_wings": [2, 0, 0, 0]}, + index=["falcon", "dog", "cat", "ant"], + ) + + result = df.value_counts() + expected = pd.Series( + data=[2, 1, 1], + index=pd.MultiIndex.from_arrays( + [(4, 6, 2), (0, 0, 2)], names=["num_legs", "num_wings"] + ), + ) + + tm.assert_series_equal(result, expected) + + +def test_data_frame_value_counts_normalize(): + df = pd.DataFrame( + {"num_legs": [2, 4, 4, 6], "num_wings": [2, 0, 0, 0]}, + index=["falcon", "dog", "cat", "ant"], + ) + + result = df.value_counts(normalize=True) + expected = pd.Series( + data=[0.5, 0.25, 0.25], + index=pd.MultiIndex.from_arrays( + [(4, 6, 2), (0, 0, 2)], names=["num_legs", "num_wings"] + ), + ) + + tm.assert_series_equal(result, expected) + + +def test_data_frame_value_counts_single_col_default(): + df = pd.DataFrame({"num_legs": [2, 4, 4, 6]}) + + result = df.value_counts() + expected = pd.Series( + data=[2, 1, 1], + index=pd.MultiIndex.from_arrays([[4, 6, 2]], names=["num_legs"]), + ) + + tm.assert_series_equal(result, expected) + + +def test_data_frame_value_counts_empty(): + df_no_cols = pd.DataFrame() + + result = df_no_cols.value_counts() + expected = pd.Series([], dtype=np.int64) + + tm.assert_series_equal(result, expected) + + +def test_data_frame_value_counts_empty_normalize(): + df_no_cols = pd.DataFrame() + + result = df_no_cols.value_counts(normalize=True) + expected = pd.Series([], dtype=np.float64) + + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 602ea9ca0471a..34df8bb57dd91 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1,4 +1,3 @@ -from collections import ChainMap from datetime import datetime, timedelta import inspect @@ -18,7 +17,6 @@ Index, IntervalIndex, MultiIndex, - RangeIndex, Series, Timestamp, cut, @@ -382,8 +380,9 @@ class Thing(frozenset): # need to stabilize repr for KeyError (due to random order in sets) def __repr__(self) -> str: tmp = sorted(self) + joined_reprs = ", ".join(map(repr, tmp)) # double curly brace prints one brace in format string - return "frozenset({{{}}})".format(", ".join(map(repr, tmp))) + return f"frozenset({{{joined_reprs}}})" thing1 = Thing(["One", "red"]) thing2 = Thing(["Two", "blue"]) @@ -532,30 +531,6 @@ def test_convert_dti_to_series(self): df.pop("ts") tm.assert_frame_equal(df, expected) - def test_reset_index_tz(self, tz_aware_fixture): - # GH 3950 - # reset_index with single level - tz = tz_aware_fixture - idx = date_range("1/1/2011", periods=5, freq="D", tz=tz, name="idx") - df = DataFrame({"a": range(5), "b": ["A", "B", "C", "D", "E"]}, index=idx) - - expected = DataFrame( - { - "idx": [ - datetime(2011, 1, 1), - datetime(2011, 1, 2), - datetime(2011, 1, 3), - datetime(2011, 1, 4), - datetime(2011, 1, 5), - ], - "a": range(5), - "b": ["A", "B", "C", "D", "E"], - }, - columns=["idx", "a", "b"], - ) - expected["idx"] = expected["idx"].apply(lambda d: Timestamp(d, tz=tz)) - tm.assert_frame_equal(df.reset_index(), expected) - def test_set_index_timezone(self): # GH 12358 # tz-aware Series should retain the tz @@ -582,17 +557,6 @@ def test_set_index_dst(self): exp = DataFrame({"b": [3, 4, 5]}, index=exp_index) tm.assert_frame_equal(res, exp) - def test_reset_index_with_intervals(self): - idx = IntervalIndex.from_breaks(np.arange(11), name="x") - original = DataFrame({"x": idx, "y": np.arange(10)})[["x", "y"]] - - result = original.set_index("x") - expected = DataFrame({"y": np.arange(10)}, index=idx) - tm.assert_frame_equal(result, expected) - - result2 = result.reset_index() - tm.assert_frame_equal(result2, original) - def test_set_index_multiindexcolumns(self): columns = MultiIndex.from_tuples([("foo", 1), ("foo", 2), ("bar", 1)]) df = DataFrame(np.random.randn(3, 3), columns=columns) @@ -651,65 +615,6 @@ def test_dti_set_index_reindex(self): # Renaming - def test_rename(self, float_frame): - mapping = {"A": "a", "B": "b", "C": "c", "D": "d"} - - renamed = float_frame.rename(columns=mapping) - renamed2 = float_frame.rename(columns=str.lower) - - tm.assert_frame_equal(renamed, renamed2) - tm.assert_frame_equal( - renamed2.rename(columns=str.upper), float_frame, check_names=False - ) - - # index - data = {"A": {"foo": 0, "bar": 1}} - - # gets sorted alphabetical - df = DataFrame(data) - renamed = df.rename(index={"foo": "bar", "bar": "foo"}) - tm.assert_index_equal(renamed.index, Index(["foo", "bar"])) - - renamed = df.rename(index=str.upper) - tm.assert_index_equal(renamed.index, Index(["BAR", "FOO"])) - - # have to pass something - with pytest.raises(TypeError, match="must pass an index to rename"): - float_frame.rename() - - # partial columns - renamed = float_frame.rename(columns={"C": "foo", "D": "bar"}) - tm.assert_index_equal(renamed.columns, Index(["A", "B", "foo", "bar"])) - - # other axis - renamed = float_frame.T.rename(index={"C": "foo", "D": "bar"}) - tm.assert_index_equal(renamed.index, Index(["A", "B", "foo", "bar"])) - - # index with name - index = Index(["foo", "bar"], name="name") - renamer = DataFrame(data, index=index) - renamed = renamer.rename(index={"foo": "bar", "bar": "foo"}) - tm.assert_index_equal(renamed.index, Index(["bar", "foo"], name="name")) - assert renamed.index.name == renamer.index.name - - @pytest.mark.parametrize( - "args,kwargs", - [ - ((ChainMap({"A": "a"}, {"B": "b"}),), dict(axis="columns")), - ((), dict(columns=ChainMap({"A": "a"}, {"B": "b"}))), - ], - ) - def test_rename_chainmap(self, args, kwargs): - # see gh-23859 - colAData = range(1, 11) - colBdata = np.random.randn(10) - - df = DataFrame({"A": colAData, "B": colBdata}) - result = df.rename(*args, **kwargs) - - expected = DataFrame({"a": colAData, "b": colBdata}) - tm.assert_frame_equal(result, expected) - def test_rename_axis_inplace(self, float_frame): # GH 15704 expected = float_frame.rename_axis("foo") @@ -784,168 +689,6 @@ def test_rename_axis_mapper(self): with pytest.raises(TypeError, match="bogus"): df.rename_axis(bogus=None) - @pytest.mark.parametrize( - "kwargs, rename_index, rename_columns", - [ - ({"mapper": None, "axis": 0}, True, False), - ({"mapper": None, "axis": 1}, False, True), - ({"index": None}, True, False), - ({"columns": None}, False, True), - ({"index": None, "columns": None}, True, True), - ({}, False, False), - ], - ) - def test_rename_axis_none(self, kwargs, rename_index, rename_columns): - # GH 25034 - index = Index(list("abc"), name="foo") - columns = Index(["col1", "col2"], name="bar") - data = np.arange(6).reshape(3, 2) - df = DataFrame(data, index, columns) - - result = df.rename_axis(**kwargs) - expected_index = index.rename(None) if rename_index else index - expected_columns = columns.rename(None) if rename_columns else columns - expected = DataFrame(data, expected_index, expected_columns) - tm.assert_frame_equal(result, expected) - - def test_rename_multiindex(self): - - tuples_index = [("foo1", "bar1"), ("foo2", "bar2")] - tuples_columns = [("fizz1", "buzz1"), ("fizz2", "buzz2")] - index = MultiIndex.from_tuples(tuples_index, names=["foo", "bar"]) - columns = MultiIndex.from_tuples(tuples_columns, names=["fizz", "buzz"]) - df = DataFrame([(0, 0), (1, 1)], index=index, columns=columns) - - # - # without specifying level -> across all levels - - renamed = df.rename( - index={"foo1": "foo3", "bar2": "bar3"}, - columns={"fizz1": "fizz3", "buzz2": "buzz3"}, - ) - new_index = MultiIndex.from_tuples( - [("foo3", "bar1"), ("foo2", "bar3")], names=["foo", "bar"] - ) - new_columns = MultiIndex.from_tuples( - [("fizz3", "buzz1"), ("fizz2", "buzz3")], names=["fizz", "buzz"] - ) - tm.assert_index_equal(renamed.index, new_index) - tm.assert_index_equal(renamed.columns, new_columns) - assert renamed.index.names == df.index.names - assert renamed.columns.names == df.columns.names - - # - # with specifying a level (GH13766) - - # dict - new_columns = MultiIndex.from_tuples( - [("fizz3", "buzz1"), ("fizz2", "buzz2")], names=["fizz", "buzz"] - ) - renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level=0) - tm.assert_index_equal(renamed.columns, new_columns) - renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level="fizz") - tm.assert_index_equal(renamed.columns, new_columns) - - new_columns = MultiIndex.from_tuples( - [("fizz1", "buzz1"), ("fizz2", "buzz3")], names=["fizz", "buzz"] - ) - renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level=1) - tm.assert_index_equal(renamed.columns, new_columns) - renamed = df.rename(columns={"fizz1": "fizz3", "buzz2": "buzz3"}, level="buzz") - tm.assert_index_equal(renamed.columns, new_columns) - - # function - func = str.upper - new_columns = MultiIndex.from_tuples( - [("FIZZ1", "buzz1"), ("FIZZ2", "buzz2")], names=["fizz", "buzz"] - ) - renamed = df.rename(columns=func, level=0) - tm.assert_index_equal(renamed.columns, new_columns) - renamed = df.rename(columns=func, level="fizz") - tm.assert_index_equal(renamed.columns, new_columns) - - new_columns = MultiIndex.from_tuples( - [("fizz1", "BUZZ1"), ("fizz2", "BUZZ2")], names=["fizz", "buzz"] - ) - renamed = df.rename(columns=func, level=1) - tm.assert_index_equal(renamed.columns, new_columns) - renamed = df.rename(columns=func, level="buzz") - tm.assert_index_equal(renamed.columns, new_columns) - - # index - new_index = MultiIndex.from_tuples( - [("foo3", "bar1"), ("foo2", "bar2")], names=["foo", "bar"] - ) - renamed = df.rename(index={"foo1": "foo3", "bar2": "bar3"}, level=0) - tm.assert_index_equal(renamed.index, new_index) - - def test_rename_nocopy(self, float_frame): - renamed = float_frame.rename(columns={"C": "foo"}, copy=False) - renamed["foo"] = 1.0 - assert (float_frame["C"] == 1.0).all() - - def test_rename_inplace(self, float_frame): - float_frame.rename(columns={"C": "foo"}) - assert "C" in float_frame - assert "foo" not in float_frame - - c_id = id(float_frame["C"]) - float_frame = float_frame.copy() - float_frame.rename(columns={"C": "foo"}, inplace=True) - - assert "C" not in float_frame - assert "foo" in float_frame - assert id(float_frame["foo"]) != c_id - - def test_rename_bug(self): - # GH 5344 - # rename set ref_locs, and set_index was not resetting - df = DataFrame({0: ["foo", "bar"], 1: ["bah", "bas"], 2: [1, 2]}) - df = df.rename(columns={0: "a"}) - df = df.rename(columns={1: "b"}) - df = df.set_index(["a", "b"]) - df.columns = ["2001-01-01"] - expected = DataFrame( - [[1], [2]], - index=MultiIndex.from_tuples( - [("foo", "bah"), ("bar", "bas")], names=["a", "b"] - ), - columns=["2001-01-01"], - ) - tm.assert_frame_equal(df, expected) - - def test_rename_bug2(self): - # GH 19497 - # rename was changing Index to MultiIndex if Index contained tuples - - df = DataFrame(data=np.arange(3), index=[(0, 0), (1, 1), (2, 2)], columns=["a"]) - df = df.rename({(1, 1): (5, 4)}, axis="index") - expected = DataFrame( - data=np.arange(3), index=[(0, 0), (5, 4), (2, 2)], columns=["a"] - ) - tm.assert_frame_equal(df, expected) - - def test_rename_errors_raises(self): - df = DataFrame(columns=["A", "B", "C", "D"]) - with pytest.raises(KeyError, match="'E'] not found in axis"): - df.rename(columns={"A": "a", "E": "e"}, errors="raise") - - @pytest.mark.parametrize( - "mapper, errors, expected_columns", - [ - ({"A": "a", "E": "e"}, "ignore", ["a", "B", "C", "D"]), - ({"A": "a"}, "raise", ["a", "B", "C", "D"]), - (str.lower, "raise", ["a", "b", "c", "d"]), - ], - ) - def test_rename_errors(self, mapper, errors, expected_columns): - # GH 13473 - # rename now works with errors parameter - df = DataFrame(columns=["A", "B", "C", "D"]) - result = df.rename(columns=mapper, errors=errors) - expected = DataFrame(columns=expected_columns) - tm.assert_frame_equal(result, expected) - def test_reorder_levels(self): index = MultiIndex( levels=[["bar"], ["one", "two", "three"], [0, 1]], @@ -984,253 +727,6 @@ def test_reorder_levels(self): result = df.reorder_levels(["L0", "L0", "L0"]) tm.assert_frame_equal(result, expected) - def test_reset_index(self, float_frame): - stacked = float_frame.stack()[::2] - stacked = DataFrame({"foo": stacked, "bar": stacked}) - - names = ["first", "second"] - stacked.index.names = names - deleveled = stacked.reset_index() - for i, (lev, level_codes) in enumerate( - zip(stacked.index.levels, stacked.index.codes) - ): - values = lev.take(level_codes) - name = names[i] - tm.assert_index_equal(values, Index(deleveled[name])) - - stacked.index.names = [None, None] - deleveled2 = stacked.reset_index() - tm.assert_series_equal( - deleveled["first"], deleveled2["level_0"], check_names=False - ) - tm.assert_series_equal( - deleveled["second"], deleveled2["level_1"], check_names=False - ) - - # default name assigned - rdf = float_frame.reset_index() - exp = Series(float_frame.index.values, name="index") - tm.assert_series_equal(rdf["index"], exp) - - # default name assigned, corner case - df = float_frame.copy() - df["index"] = "foo" - rdf = df.reset_index() - exp = Series(float_frame.index.values, name="level_0") - tm.assert_series_equal(rdf["level_0"], exp) - - # but this is ok - float_frame.index.name = "index" - deleveled = float_frame.reset_index() - tm.assert_series_equal(deleveled["index"], Series(float_frame.index)) - tm.assert_index_equal(deleveled.index, Index(np.arange(len(deleveled)))) - - # preserve column names - float_frame.columns.name = "columns" - resetted = float_frame.reset_index() - assert resetted.columns.name == "columns" - - # only remove certain columns - df = float_frame.reset_index().set_index(["index", "A", "B"]) - rs = df.reset_index(["A", "B"]) - - # TODO should reset_index check_names ? - tm.assert_frame_equal(rs, float_frame, check_names=False) - - rs = df.reset_index(["index", "A", "B"]) - tm.assert_frame_equal(rs, float_frame.reset_index(), check_names=False) - - rs = df.reset_index(["index", "A", "B"]) - tm.assert_frame_equal(rs, float_frame.reset_index(), check_names=False) - - rs = df.reset_index("A") - xp = float_frame.reset_index().set_index(["index", "B"]) - tm.assert_frame_equal(rs, xp, check_names=False) - - # test resetting in place - df = float_frame.copy() - resetted = float_frame.reset_index() - df.reset_index(inplace=True) - tm.assert_frame_equal(df, resetted, check_names=False) - - df = float_frame.reset_index().set_index(["index", "A", "B"]) - rs = df.reset_index("A", drop=True) - xp = float_frame.copy() - del xp["A"] - xp = xp.set_index(["B"], append=True) - tm.assert_frame_equal(rs, xp, check_names=False) - - def test_reset_index_name(self): - df = DataFrame( - [[1, 2, 3, 4], [5, 6, 7, 8]], - columns=["A", "B", "C", "D"], - index=Index(range(2), name="x"), - ) - assert df.reset_index().index.name is None - assert df.reset_index(drop=True).index.name is None - df.reset_index(inplace=True) - assert df.index.name is None - - def test_reset_index_level(self): - df = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]], columns=["A", "B", "C", "D"]) - - for levels in ["A", "B"], [0, 1]: - # With MultiIndex - result = df.set_index(["A", "B"]).reset_index(level=levels[0]) - tm.assert_frame_equal(result, df.set_index("B")) - - result = df.set_index(["A", "B"]).reset_index(level=levels[:1]) - tm.assert_frame_equal(result, df.set_index("B")) - - result = df.set_index(["A", "B"]).reset_index(level=levels) - tm.assert_frame_equal(result, df) - - result = df.set_index(["A", "B"]).reset_index(level=levels, drop=True) - tm.assert_frame_equal(result, df[["C", "D"]]) - - # With single-level Index (GH 16263) - result = df.set_index("A").reset_index(level=levels[0]) - tm.assert_frame_equal(result, df) - - result = df.set_index("A").reset_index(level=levels[:1]) - tm.assert_frame_equal(result, df) - - result = df.set_index(["A"]).reset_index(level=levels[0], drop=True) - tm.assert_frame_equal(result, df[["B", "C", "D"]]) - - # Missing levels - for both MultiIndex and single-level Index: - for idx_lev in ["A", "B"], ["A"]: - with pytest.raises(KeyError, match=r"(L|l)evel \(?E\)?"): - df.set_index(idx_lev).reset_index(level=["A", "E"]) - with pytest.raises(IndexError, match="Too many levels"): - df.set_index(idx_lev).reset_index(level=[0, 1, 2]) - - def test_reset_index_right_dtype(self): - time = np.arange(0.0, 10, np.sqrt(2) / 2) - s1 = Series( - (9.81 * time ** 2) / 2, index=Index(time, name="time"), name="speed" - ) - df = DataFrame(s1) - - resetted = s1.reset_index() - assert resetted["time"].dtype == np.float64 - - resetted = df.reset_index() - assert resetted["time"].dtype == np.float64 - - def test_reset_index_multiindex_col(self): - vals = np.random.randn(3, 3).astype(object) - idx = ["x", "y", "z"] - full = np.hstack(([[x] for x in idx], vals)) - df = DataFrame( - vals, - Index(idx, name="a"), - columns=[["b", "b", "c"], ["mean", "median", "mean"]], - ) - rs = df.reset_index() - xp = DataFrame( - full, columns=[["a", "b", "b", "c"], ["", "mean", "median", "mean"]] - ) - tm.assert_frame_equal(rs, xp) - - rs = df.reset_index(col_fill=None) - xp = DataFrame( - full, columns=[["a", "b", "b", "c"], ["a", "mean", "median", "mean"]] - ) - tm.assert_frame_equal(rs, xp) - - rs = df.reset_index(col_level=1, col_fill="blah") - xp = DataFrame( - full, columns=[["blah", "b", "b", "c"], ["a", "mean", "median", "mean"]] - ) - tm.assert_frame_equal(rs, xp) - - df = DataFrame( - vals, - MultiIndex.from_arrays([[0, 1, 2], ["x", "y", "z"]], names=["d", "a"]), - columns=[["b", "b", "c"], ["mean", "median", "mean"]], - ) - rs = df.reset_index("a") - xp = DataFrame( - full, - Index([0, 1, 2], name="d"), - columns=[["a", "b", "b", "c"], ["", "mean", "median", "mean"]], - ) - tm.assert_frame_equal(rs, xp) - - rs = df.reset_index("a", col_fill=None) - xp = DataFrame( - full, - Index(range(3), name="d"), - columns=[["a", "b", "b", "c"], ["a", "mean", "median", "mean"]], - ) - tm.assert_frame_equal(rs, xp) - - rs = df.reset_index("a", col_fill="blah", col_level=1) - xp = DataFrame( - full, - Index(range(3), name="d"), - columns=[["blah", "b", "b", "c"], ["a", "mean", "median", "mean"]], - ) - tm.assert_frame_equal(rs, xp) - - def test_reset_index_multiindex_nan(self): - # GH6322, testing reset_index on MultiIndexes - # when we have a nan or all nan - df = DataFrame( - {"A": ["a", "b", "c"], "B": [0, 1, np.nan], "C": np.random.rand(3)} - ) - rs = df.set_index(["A", "B"]).reset_index() - tm.assert_frame_equal(rs, df) - - df = DataFrame( - {"A": [np.nan, "b", "c"], "B": [0, 1, 2], "C": np.random.rand(3)} - ) - rs = df.set_index(["A", "B"]).reset_index() - tm.assert_frame_equal(rs, df) - - df = DataFrame({"A": ["a", "b", "c"], "B": [0, 1, 2], "C": [np.nan, 1.1, 2.2]}) - rs = df.set_index(["A", "B"]).reset_index() - tm.assert_frame_equal(rs, df) - - df = DataFrame( - { - "A": ["a", "b", "c"], - "B": [np.nan, np.nan, np.nan], - "C": np.random.rand(3), - } - ) - rs = df.set_index(["A", "B"]).reset_index() - tm.assert_frame_equal(rs, df) - - def test_reset_index_with_datetimeindex_cols(self): - # GH5818 - # - df = DataFrame( - [[1, 2], [3, 4]], - columns=date_range("1/1/2013", "1/2/2013"), - index=["A", "B"], - ) - - result = df.reset_index() - expected = DataFrame( - [["A", 1, 2], ["B", 3, 4]], - columns=["index", datetime(2013, 1, 1), datetime(2013, 1, 2)], - ) - tm.assert_frame_equal(result, expected) - - def test_reset_index_range(self): - # GH 12071 - df = DataFrame([[0, 0], [1, 1]], columns=["A", "B"], index=RangeIndex(stop=2)) - result = df.reset_index() - assert isinstance(result.index, RangeIndex) - expected = DataFrame( - [[0, 0, 0], [1, 1, 1]], - columns=["index", "A", "B"], - index=RangeIndex(stop=2), - ) - tm.assert_frame_equal(result, expected) - def test_set_index_names(self): df = tm.makeDataFrame() df.index.name = "name" @@ -1261,92 +757,6 @@ def test_set_index_names(self): # Check equality tm.assert_index_equal(df.set_index([df.index, idx2]).index, mi2) - def test_rename_objects(self, float_string_frame): - renamed = float_string_frame.rename(columns=str.upper) - - assert "FOO" in renamed - assert "foo" not in renamed - - def test_rename_axis_style(self): - # https://github.com/pandas-dev/pandas/issues/12392 - df = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["X", "Y"]) - expected = DataFrame({"a": [1, 2], "b": [1, 2]}, index=["X", "Y"]) - - result = df.rename(str.lower, axis=1) - tm.assert_frame_equal(result, expected) - - result = df.rename(str.lower, axis="columns") - tm.assert_frame_equal(result, expected) - - result = df.rename({"A": "a", "B": "b"}, axis=1) - tm.assert_frame_equal(result, expected) - - result = df.rename({"A": "a", "B": "b"}, axis="columns") - tm.assert_frame_equal(result, expected) - - # Index - expected = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["x", "y"]) - result = df.rename(str.lower, axis=0) - tm.assert_frame_equal(result, expected) - - result = df.rename(str.lower, axis="index") - tm.assert_frame_equal(result, expected) - - result = df.rename({"X": "x", "Y": "y"}, axis=0) - tm.assert_frame_equal(result, expected) - - result = df.rename({"X": "x", "Y": "y"}, axis="index") - tm.assert_frame_equal(result, expected) - - result = df.rename(mapper=str.lower, axis="index") - tm.assert_frame_equal(result, expected) - - def test_rename_mapper_multi(self): - df = DataFrame({"A": ["a", "b"], "B": ["c", "d"], "C": [1, 2]}).set_index( - ["A", "B"] - ) - result = df.rename(str.upper) - expected = df.rename(index=str.upper) - tm.assert_frame_equal(result, expected) - - def test_rename_positional_named(self): - # https://github.com/pandas-dev/pandas/issues/12392 - df = DataFrame({"a": [1, 2], "b": [1, 2]}, index=["X", "Y"]) - result = df.rename(index=str.lower, columns=str.upper) - expected = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["x", "y"]) - tm.assert_frame_equal(result, expected) - - def test_rename_axis_style_raises(self): - # see gh-12392 - df = DataFrame({"A": [1, 2], "B": [1, 2]}, index=["0", "1"]) - - # Named target and axis - over_spec_msg = "Cannot specify both 'axis' and any of 'index' or 'columns'" - with pytest.raises(TypeError, match=over_spec_msg): - df.rename(index=str.lower, axis=1) - - with pytest.raises(TypeError, match=over_spec_msg): - df.rename(index=str.lower, axis="columns") - - with pytest.raises(TypeError, match=over_spec_msg): - df.rename(columns=str.lower, axis="columns") - - with pytest.raises(TypeError, match=over_spec_msg): - df.rename(index=str.lower, axis=0) - - # Multiple targets and axis - with pytest.raises(TypeError, match=over_spec_msg): - df.rename(str.lower, index=str.lower, axis="columns") - - # Too many targets - over_spec_msg = "Cannot specify both 'mapper' and any of 'index' or 'columns'" - with pytest.raises(TypeError, match=over_spec_msg): - df.rename(str.lower, index=str.lower, columns=str.lower) - - # Duplicates - with pytest.raises(TypeError, match="multiple values"): - df.rename(id, mapper=id) - def test_reindex_api_equivalence(self): # equivalence of the labels/axis and index/columns API's df = DataFrame( @@ -1375,43 +785,6 @@ def test_reindex_api_equivalence(self): for res in [res2, res3]: tm.assert_frame_equal(res1, res) - def test_rename_positional_raises(self): - # GH 29136 - df = DataFrame(columns=["A", "B"]) - msg = r"rename\(\) takes from 1 to 2 positional arguments" - - with pytest.raises(TypeError, match=msg): - df.rename(None, str.lower) - - def test_rename_no_mappings_raises(self): - # GH 29136 - df = DataFrame([[1]]) - msg = "must pass an index to rename" - with pytest.raises(TypeError, match=msg): - df.rename() - - with pytest.raises(TypeError, match=msg): - df.rename(None, index=None) - - with pytest.raises(TypeError, match=msg): - df.rename(None, columns=None) - - with pytest.raises(TypeError, match=msg): - df.rename(None, columns=None, index=None) - - def test_rename_mapper_and_positional_arguments_raises(self): - # GH 29136 - df = DataFrame([[1]]) - msg = "Cannot specify both 'mapper' and any of 'index' or 'columns'" - with pytest.raises(TypeError, match=msg): - df.rename({}, index={}) - - with pytest.raises(TypeError, match=msg): - df.rename({}, columns={}) - - with pytest.raises(TypeError, match=msg): - df.rename({}, columns={}, index={}) - def test_assign_columns(self, float_frame): float_frame["hi"] = "there" @@ -1467,25 +840,6 @@ def test_reindex_signature(self): "tolerance", } - def test_droplevel(self): - # GH20342 - df = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) - df = df.set_index([0, 1]).rename_axis(["a", "b"]) - df.columns = MultiIndex.from_tuples( - [("c", "e"), ("d", "f")], names=["level_1", "level_2"] - ) - - # test that dropping of a level in index works - expected = df.reset_index("a", drop=True) - result = df.droplevel("a", axis="index") - tm.assert_frame_equal(result, expected) - - # test that dropping of a level in columns works - expected = df.copy() - expected.columns = Index(["c", "d"], name="level_1") - result = df.droplevel("level_2", axis="columns") - tm.assert_frame_equal(result, expected) - class TestIntervalIndex: def test_setitem(self): diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py index 25b2997eb088f..61802956addeb 100644 --- a/pandas/tests/frame/test_analytics.py +++ b/pandas/tests/frame/test_analytics.py @@ -60,16 +60,18 @@ def assert_stat_op_calc( skipna_alternative : function, default None NaN-safe version of alternative """ - f = getattr(frame, opname) if check_dates: + expected_warning = FutureWarning if opname in ["mean", "median"] else None df = DataFrame({"b": date_range("1/1/2001", periods=2)}) - result = getattr(df, opname)() + with tm.assert_produces_warning(expected_warning): + result = getattr(df, opname)() assert isinstance(result, Series) df["a"] = range(len(df)) - result = getattr(df, opname)() + with tm.assert_produces_warning(expected_warning): + result = getattr(df, opname)() assert isinstance(result, Series) assert len(result) @@ -150,7 +152,6 @@ def assert_stat_op_api(opname, float_frame, float_string_frame, has_numeric_only has_numeric_only : bool, default False Whether the method "opname" has the kwarg "numeric_only" """ - # make sure works on mixed-type frame getattr(float_string_frame, opname)(axis=0) getattr(float_string_frame, opname)(axis=1) @@ -178,7 +179,6 @@ def assert_bool_op_calc(opname, alternative, frame, has_skipna=True): has_skipna : bool, default True Whether the method "opname" has the kwarg "skip_na" """ - f = getattr(frame, opname) if has_skipna: @@ -460,7 +460,8 @@ def test_nunique(self): def test_mean_mixed_datetime_numeric(self, tz): # https://github.com/pandas-dev/pandas/issues/24752 df = pd.DataFrame({"A": [1, 1], "B": [pd.Timestamp("2000", tz=tz)] * 2}) - result = df.mean() + with tm.assert_produces_warning(FutureWarning): + result = df.mean() expected = pd.Series([1.0], index=["A"]) tm.assert_series_equal(result, expected) @@ -470,7 +471,9 @@ def test_mean_excludes_datetimes(self, tz): # Our long-term desired behavior is unclear, but the behavior in # 0.24.0rc1 was buggy. df = pd.DataFrame({"A": [pd.Timestamp("2000", tz=tz)] * 2}) - result = df.mean() + with tm.assert_produces_warning(FutureWarning): + result = df.mean() + expected = pd.Series(dtype=np.float64) tm.assert_series_equal(result, expected) @@ -866,7 +869,9 @@ def test_mean_datetimelike(self): expected = pd.Series({"A": 1.0}) tm.assert_series_equal(result, expected) - result = df.mean() + with tm.assert_produces_warning(FutureWarning): + # in the future datetime columns will be included + result = df.mean() expected = pd.Series({"A": 1.0, "C": df.loc[1, "C"]}) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/frame/test_api.py b/pandas/tests/frame/test_api.py index 17cc50661e3cb..a021dd91a7d26 100644 --- a/pandas/tests/frame/test_api.py +++ b/pandas/tests/frame/test_api.py @@ -46,19 +46,19 @@ def test_get_value(self, float_frame): def test_add_prefix_suffix(self, float_frame): with_prefix = float_frame.add_prefix("foo#") - expected = pd.Index(["foo#{c}".format(c=c) for c in float_frame.columns]) + expected = pd.Index([f"foo#{c}" for c in float_frame.columns]) tm.assert_index_equal(with_prefix.columns, expected) with_suffix = float_frame.add_suffix("#foo") - expected = pd.Index(["{c}#foo".format(c=c) for c in float_frame.columns]) + expected = pd.Index([f"{c}#foo" for c in float_frame.columns]) tm.assert_index_equal(with_suffix.columns, expected) with_pct_prefix = float_frame.add_prefix("%") - expected = pd.Index(["%{c}".format(c=c) for c in float_frame.columns]) + expected = pd.Index([f"%{c}" for c in float_frame.columns]) tm.assert_index_equal(with_pct_prefix.columns, expected) with_pct_suffix = float_frame.add_suffix("%") - expected = pd.Index(["{c}%".format(c=c) for c in float_frame.columns]) + expected = pd.Index([f"{c}%" for c in float_frame.columns]) tm.assert_index_equal(with_pct_suffix.columns, expected) def test_get_axis(self, float_frame): diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index c6eacf2bbcd84..e4be8a979a70f 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -4,6 +4,7 @@ import numpy as np import pytest +import pytz import pandas as pd import pandas._testing as tm @@ -711,6 +712,25 @@ def test_operations_with_interval_categories_index(self, all_arithmetic_operator expected = pd.DataFrame([[getattr(n, op)(num) for n in data]], columns=ind) tm.assert_frame_equal(result, expected) + def test_frame_with_frame_reindex(self): + # GH#31623 + df = pd.DataFrame( + { + "foo": [pd.Timestamp("2019"), pd.Timestamp("2020")], + "bar": [pd.Timestamp("2018"), pd.Timestamp("2021")], + }, + columns=["foo", "bar"], + ) + df2 = df[["foo"]] + + result = df - df2 + + expected = pd.DataFrame( + {"foo": [pd.Timedelta(0), pd.Timedelta(0)], "bar": [np.nan, np.nan]}, + columns=["bar", "foo"], + ) + tm.assert_frame_equal(result, expected) + def test_frame_with_zero_len_series_corner_cases(): # GH#28600 @@ -752,3 +772,35 @@ def test_frame_single_columns_object_sum_axis_1(): result = df.sum(axis=1) expected = pd.Series(["A", 1.2, 0]) tm.assert_series_equal(result, expected) + + +# ------------------------------------------------------------------- +# Unsorted +# These arithmetic tests were previously in other files, eventually +# should be parametrized and put into tests.arithmetic + + +class TestFrameArithmeticUnsorted: + def test_frame_add_tz_mismatch_converts_to_utc(self): + rng = pd.date_range("1/1/2011", periods=10, freq="H", tz="US/Eastern") + df = pd.DataFrame(np.random.randn(len(rng)), index=rng, columns=["a"]) + + df_moscow = df.tz_convert("Europe/Moscow") + result = df + df_moscow + assert result.index.tz is pytz.utc + + result = df_moscow + df + assert result.index.tz is pytz.utc + + def test_align_frame(self): + rng = pd.period_range("1/1/2000", "1/1/2010", freq="A") + ts = pd.DataFrame(np.random.randn(len(rng), 3), index=rng) + + result = ts + ts[::2] + expected = ts + ts + expected.values[1::2] = np.nan + tm.assert_frame_equal(result, expected) + + half = ts[::2] + result = ts + half.take(np.random.permutation(len(half))) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/test_block_internals.py b/pandas/tests/frame/test_block_internals.py index d301ed969789e..a5f5e6f36cd58 100644 --- a/pandas/tests/frame/test_block_internals.py +++ b/pandas/tests/frame/test_block_internals.py @@ -364,14 +364,14 @@ def test_pickle(self, float_string_frame, timezone_frame): def test_consolidate_datetime64(self): # numpy vstack bug - data = """\ -starting,ending,measure -2012-06-21 00:00,2012-06-23 07:00,77 -2012-06-23 07:00,2012-06-23 16:30,65 -2012-06-23 16:30,2012-06-25 08:00,77 -2012-06-25 08:00,2012-06-26 12:00,0 -2012-06-26 12:00,2012-06-27 08:00,77 -""" + data = ( + "starting,ending,measure\n" + "2012-06-21 00:00,2012-06-23 07:00,77\n" + "2012-06-23 07:00,2012-06-23 16:30,65\n" + "2012-06-23 16:30,2012-06-25 08:00,77\n" + "2012-06-25 08:00,2012-06-26 12:00,0\n" + "2012-06-26 12:00,2012-06-27 08:00,77\n" + ) df = pd.read_csv(StringIO(data), parse_dates=[0, 1]) ser_starting = df.starting @@ -397,9 +397,6 @@ def test_is_mixed_type(self, float_frame, float_string_frame): assert float_string_frame._is_mixed_type def test_get_numeric_data(self): - # TODO(wesm): unused? - intname = np.dtype(np.int_).name # noqa - floatname = np.dtype(np.float_).name # noqa datetime64name = np.dtype("M8[ns]").name objectname = np.dtype(np.object_).name @@ -581,6 +578,7 @@ def test_get_X_columns(self): tm.assert_index_equal(df._get_numeric_data().columns, pd.Index(["a", "b", "e"])) def test_strange_column_corruption_issue(self): + # FIXME: dont leave commented-out # (wesm) Unclear how exactly this is related to internal matters df = DataFrame(index=[0, 1]) df[0] = np.nan diff --git a/pandas/tests/frame/test_combine_concat.py b/pandas/tests/frame/test_combine_concat.py index 36a476d195fe5..321eb5fe94daf 100644 --- a/pandas/tests/frame/test_combine_concat.py +++ b/pandas/tests/frame/test_combine_concat.py @@ -21,27 +21,6 @@ def test_concat_multiple_frames_dtypes(self): ) tm.assert_series_equal(results, expected) - @pytest.mark.parametrize( - "data", - [ - pd.date_range("2000", periods=4), - pd.date_range("2000", periods=4, tz="US/Central"), - pd.period_range("2000", periods=4), - pd.timedelta_range(0, periods=4), - ], - ) - def test_combine_datetlike_udf(self, data): - # https://github.com/pandas-dev/pandas/issues/23079 - df = pd.DataFrame({"A": data}) - other = df.copy() - df.iloc[1, 0] = None - - def combiner(a, b): - return b - - result = df.combine(other, combiner) - tm.assert_frame_equal(result, other) - def test_concat_multiple_tzs(self): # GH 12467 # combining datetime tz-aware and naive DataFrames diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 5f4c78449f71d..071d2409f1be2 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -7,8 +7,10 @@ import numpy.ma as ma import numpy.ma.mrecords as mrecords import pytest +import pytz from pandas.compat import is_platform_little_endian +from pandas.compat.numpy import _is_numpy_dev from pandas.core.dtypes.common import is_integer_dtype @@ -144,6 +146,7 @@ def test_constructor_dtype_list_data(self): assert df.loc[1, 0] is None assert df.loc[0, 1] == "2" + @pytest.mark.xfail(_is_numpy_dev, reason="Interprets list of frame as 3D") def test_constructor_list_frames(self): # see gh-3243 result = DataFrame([DataFrame()]) @@ -278,7 +281,7 @@ def test_constructor_ordereddict(self): nitems = 100 nums = list(range(nitems)) random.shuffle(nums) - expected = ["A{i:d}".format(i=i) for i in nums] + expected = [f"A{i:d}" for i in nums] df = DataFrame(OrderedDict(zip(expected, [[0]] * nitems))) assert expected == list(df.columns) @@ -316,7 +319,8 @@ def test_constructor_dict(self): # mix dict and array, wrong size - no spec for which error should raise # first - with pytest.raises(ValueError): + msg = "Mixing dicts with non-Series may lead to ambiguous ordering." + with pytest.raises(ValueError, match=msg): DataFrame({"A": {"a": "a", "b": "b"}, "B": ["a", "b", "c"]}) # Length-one dict micro-optimization @@ -502,6 +506,7 @@ def test_constructor_error_msgs(self): with pytest.raises(ValueError, match=msg): DataFrame({"a": False, "b": True}) + @pytest.mark.xfail(_is_numpy_dev, reason="Interprets embedded frame as 3D") def test_constructor_with_embedded_frames(self): # embedded data frames @@ -1859,11 +1864,7 @@ def check(df): # No NaN found -> error if len(indexer) == 0: - msg = ( - "cannot do label indexing on RangeIndex " - r"with these indexers \[nan\] of type float" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^nan$"): df.loc[:, np.nan] # single nan should result in Series elif len(indexer) == 1: @@ -2385,6 +2386,12 @@ def test_from_records_series_list_dict(self): result = DataFrame.from_records(data) tm.assert_frame_equal(result, expected) + def test_frame_from_records_utc(self): + rec = {"datum": 1.5, "begin_time": datetime(2006, 4, 27, tzinfo=pytz.utc)} + + # it works + DataFrame.from_records([rec], index="begin_time") + def test_to_frame_with_falsey_names(self): # GH 16114 result = Series(name=0, dtype=object).to_frame().dtypes @@ -2456,6 +2463,18 @@ def test_construct_with_two_categoricalindex_series(self): ) tm.assert_frame_equal(result, expected) + def test_from_M8_structured(self): + dates = [(datetime(2012, 9, 9, 0, 0), datetime(2012, 9, 8, 15, 10))] + arr = np.array(dates, dtype=[("Date", "M8[us]"), ("Forecasting", "M8[us]")]) + df = DataFrame(arr) + + assert df["Date"][0] == dates[0][0] + assert df["Forecasting"][0] == dates[0][1] + + s = Series(arr["Date"]) + assert isinstance(s[0], Timestamp) + assert s[0] == dates[0][0] + class TestDataFrameConstructorWithDatetimeTZ: def test_from_dict(self): diff --git a/pandas/tests/frame/test_cumulative.py b/pandas/tests/frame/test_cumulative.py index b545d6aa8afd3..486cbfb2761e0 100644 --- a/pandas/tests/frame/test_cumulative.py +++ b/pandas/tests/frame/test_cumulative.py @@ -7,8 +7,9 @@ """ import numpy as np +import pytest -from pandas import DataFrame, Series +from pandas import DataFrame, Series, _is_numpy_dev import pandas._testing as tm @@ -73,6 +74,11 @@ def test_cumprod(self, datetime_frame): df.cumprod(0) df.cumprod(1) + @pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, + ) def test_cummin(self, datetime_frame): datetime_frame.loc[5:10, 0] = np.nan datetime_frame.loc[10:15, 1] = np.nan @@ -96,6 +102,11 @@ def test_cummin(self, datetime_frame): cummin_xs = datetime_frame.cummin(axis=1) assert np.shape(cummin_xs) == np.shape(datetime_frame) + @pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, + ) def test_cummax(self, datetime_frame): datetime_frame.loc[5:10, 0] = np.nan datetime_frame.loc[10:15, 1] = np.nan diff --git a/pandas/tests/frame/test_dtypes.py b/pandas/tests/frame/test_dtypes.py index 966f0d416676c..713d8f3ceeedb 100644 --- a/pandas/tests/frame/test_dtypes.py +++ b/pandas/tests/frame/test_dtypes.py @@ -111,325 +111,6 @@ def test_dtypes_are_correct_after_column_slice(self): pd.Series(odict([("a", np.float_), ("b", np.float_), ("c", np.float_)])), ) - def test_select_dtypes_include_using_list_like(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.Categorical(list("abc")), - "g": pd.date_range("20130101", periods=3), - "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), - "i": pd.date_range("20130101", periods=3, tz="CET"), - "j": pd.period_range("2013-01", periods=3, freq="M"), - "k": pd.timedelta_range("1 day", periods=3), - } - ) - - ri = df.select_dtypes(include=[np.number]) - ei = df[["b", "c", "d", "k"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include=[np.number], exclude=["timedelta"]) - ei = df[["b", "c", "d"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include=[np.number, "category"], exclude=["timedelta"]) - ei = df[["b", "c", "d", "f"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include=["datetime"]) - ei = df[["g"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include=["datetime64"]) - ei = df[["g"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include=["datetimetz"]) - ei = df[["h", "i"]] - tm.assert_frame_equal(ri, ei) - - with pytest.raises(NotImplementedError, match=r"^$"): - df.select_dtypes(include=["period"]) - - def test_select_dtypes_exclude_using_list_like(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - } - ) - re = df.select_dtypes(exclude=[np.number]) - ee = df[["a", "e"]] - tm.assert_frame_equal(re, ee) - - def test_select_dtypes_exclude_include_using_list_like(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.date_range("now", periods=3).values, - } - ) - exclude = (np.datetime64,) - include = np.bool_, "integer" - r = df.select_dtypes(include=include, exclude=exclude) - e = df[["b", "c", "e"]] - tm.assert_frame_equal(r, e) - - exclude = ("datetime",) - include = "bool", "int64", "int32" - r = df.select_dtypes(include=include, exclude=exclude) - e = df[["b", "e"]] - tm.assert_frame_equal(r, e) - - def test_select_dtypes_include_using_scalars(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.Categorical(list("abc")), - "g": pd.date_range("20130101", periods=3), - "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), - "i": pd.date_range("20130101", periods=3, tz="CET"), - "j": pd.period_range("2013-01", periods=3, freq="M"), - "k": pd.timedelta_range("1 day", periods=3), - } - ) - - ri = df.select_dtypes(include=np.number) - ei = df[["b", "c", "d", "k"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include="datetime") - ei = df[["g"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include="datetime64") - ei = df[["g"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include="category") - ei = df[["f"]] - tm.assert_frame_equal(ri, ei) - - with pytest.raises(NotImplementedError, match=r"^$"): - df.select_dtypes(include="period") - - def test_select_dtypes_exclude_using_scalars(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.Categorical(list("abc")), - "g": pd.date_range("20130101", periods=3), - "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), - "i": pd.date_range("20130101", periods=3, tz="CET"), - "j": pd.period_range("2013-01", periods=3, freq="M"), - "k": pd.timedelta_range("1 day", periods=3), - } - ) - - ri = df.select_dtypes(exclude=np.number) - ei = df[["a", "e", "f", "g", "h", "i", "j"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(exclude="category") - ei = df[["a", "b", "c", "d", "e", "g", "h", "i", "j", "k"]] - tm.assert_frame_equal(ri, ei) - - with pytest.raises(NotImplementedError, match=r"^$"): - df.select_dtypes(exclude="period") - - def test_select_dtypes_include_exclude_using_scalars(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.Categorical(list("abc")), - "g": pd.date_range("20130101", periods=3), - "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), - "i": pd.date_range("20130101", periods=3, tz="CET"), - "j": pd.period_range("2013-01", periods=3, freq="M"), - "k": pd.timedelta_range("1 day", periods=3), - } - ) - - ri = df.select_dtypes(include=np.number, exclude="floating") - ei = df[["b", "c", "k"]] - tm.assert_frame_equal(ri, ei) - - def test_select_dtypes_include_exclude_mixed_scalars_lists(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.Categorical(list("abc")), - "g": pd.date_range("20130101", periods=3), - "h": pd.date_range("20130101", periods=3, tz="US/Eastern"), - "i": pd.date_range("20130101", periods=3, tz="CET"), - "j": pd.period_range("2013-01", periods=3, freq="M"), - "k": pd.timedelta_range("1 day", periods=3), - } - ) - - ri = df.select_dtypes(include=np.number, exclude=["floating", "timedelta"]) - ei = df[["b", "c"]] - tm.assert_frame_equal(ri, ei) - - ri = df.select_dtypes(include=[np.number, "category"], exclude="floating") - ei = df[["b", "c", "f", "k"]] - tm.assert_frame_equal(ri, ei) - - def test_select_dtypes_duplicate_columns(self): - # GH20839 - odict = OrderedDict - df = DataFrame( - odict( - [ - ("a", list("abc")), - ("b", list(range(1, 4))), - ("c", np.arange(3, 6).astype("u1")), - ("d", np.arange(4.0, 7.0, dtype="float64")), - ("e", [True, False, True]), - ("f", pd.date_range("now", periods=3).values), - ] - ) - ) - df.columns = ["a", "a", "b", "b", "b", "c"] - - expected = DataFrame( - {"a": list(range(1, 4)), "b": np.arange(3, 6).astype("u1")} - ) - - result = df.select_dtypes(include=[np.number], exclude=["floating"]) - tm.assert_frame_equal(result, expected) - - def test_select_dtypes_not_an_attr_but_still_valid_dtype(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.date_range("now", periods=3).values, - } - ) - df["g"] = df.f.diff() - assert not hasattr(np, "u8") - r = df.select_dtypes(include=["i8", "O"], exclude=["timedelta"]) - e = df[["a", "b"]] - tm.assert_frame_equal(r, e) - - r = df.select_dtypes(include=["i8", "O", "timedelta64[ns]"]) - e = df[["a", "b", "g"]] - tm.assert_frame_equal(r, e) - - def test_select_dtypes_empty(self): - df = DataFrame({"a": list("abc"), "b": list(range(1, 4))}) - msg = "at least one of include or exclude must be nonempty" - with pytest.raises(ValueError, match=msg): - df.select_dtypes() - - def test_select_dtypes_bad_datetime64(self): - df = DataFrame( - { - "a": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.date_range("now", periods=3).values, - } - ) - with pytest.raises(ValueError, match=".+ is too specific"): - df.select_dtypes(include=["datetime64[D]"]) - - with pytest.raises(ValueError, match=".+ is too specific"): - df.select_dtypes(exclude=["datetime64[as]"]) - - def test_select_dtypes_datetime_with_tz(self): - - df2 = DataFrame( - dict( - A=Timestamp("20130102", tz="US/Eastern"), - B=Timestamp("20130603", tz="CET"), - ), - index=range(5), - ) - df3 = pd.concat([df2.A.to_frame(), df2.B.to_frame()], axis=1) - result = df3.select_dtypes(include=["datetime64[ns]"]) - expected = df3.reindex(columns=[]) - tm.assert_frame_equal(result, expected) - - @pytest.mark.parametrize( - "dtype", [str, "str", np.string_, "S1", "unicode", np.unicode_, "U1"] - ) - @pytest.mark.parametrize("arg", ["include", "exclude"]) - def test_select_dtypes_str_raises(self, dtype, arg): - df = DataFrame( - { - "a": list("abc"), - "g": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.date_range("now", periods=3).values, - } - ) - msg = "string dtypes are not allowed" - kwargs = {arg: [dtype]} - - with pytest.raises(TypeError, match=msg): - df.select_dtypes(**kwargs) - - def test_select_dtypes_bad_arg_raises(self): - df = DataFrame( - { - "a": list("abc"), - "g": list("abc"), - "b": list(range(1, 4)), - "c": np.arange(3, 6).astype("u1"), - "d": np.arange(4.0, 7.0, dtype="float64"), - "e": [True, False, True], - "f": pd.date_range("now", periods=3).values, - } - ) - - msg = "data type.*not understood" - with pytest.raises(TypeError, match=msg): - df.select_dtypes(["blargy, blarg, blarg"]) - - def test_select_dtypes_typecodes(self): - # GH 11990 - df = tm.makeCustomDataframe(30, 3, data_gen_f=lambda x, y: np.random.random()) - expected = df - FLOAT_TYPES = list(np.typecodes["AllFloat"]) - tm.assert_frame_equal(df.select_dtypes(FLOAT_TYPES), expected) - def test_dtypes_gh8722(self, float_string_frame): float_string_frame["bool"] = float_string_frame["A"] > 0 result = float_string_frame.dtypes @@ -702,7 +383,7 @@ def test_astype_categorical(self, dtype): @pytest.mark.parametrize("cls", [CategoricalDtype, DatetimeTZDtype, IntervalDtype]) def test_astype_categoricaldtype_class_raises(self, cls): df = DataFrame({"A": ["a", "a", "b", "c"]}) - xpr = "Expected an instance of {}".format(cls.__name__) + xpr = f"Expected an instance of {cls.__name__}" with pytest.raises(TypeError, match=xpr): df.astype({"A": cls}) @@ -827,7 +508,7 @@ def test_df_where_change_dtype(self): def test_astype_from_datetimelike_to_objectt(self, dtype, unit): # tests astype to object dtype # gh-19223 / gh-12425 - dtype = "{}[{}]".format(dtype, unit) + dtype = f"{dtype}[{unit}]" arr = np.array([[1, 2, 3]], dtype=dtype) df = DataFrame(arr) result = df.astype(object) @@ -844,7 +525,7 @@ def test_astype_from_datetimelike_to_objectt(self, dtype, unit): def test_astype_to_datetimelike_unit(self, arr_dtype, dtype, unit): # tests all units from numeric origination # gh-19223 / gh-12425 - dtype = "{}[{}]".format(dtype, unit) + dtype = f"{dtype}[{unit}]" arr = np.array([[1, 2, 3]], dtype=arr_dtype) df = DataFrame(arr) result = df.astype(dtype) @@ -856,7 +537,7 @@ def test_astype_to_datetimelike_unit(self, arr_dtype, dtype, unit): def test_astype_to_datetime_unit(self, unit): # tests all units from datetime origination # gh-19223 - dtype = "M8[{}]".format(unit) + dtype = f"M8[{unit}]" arr = np.array([[1, 2, 3]], dtype=dtype) df = DataFrame(arr) result = df.astype(dtype) @@ -868,7 +549,7 @@ def test_astype_to_datetime_unit(self, unit): def test_astype_to_timedelta_unit_ns(self, unit): # preserver the timedelta conversion # gh-19223 - dtype = "m8[{}]".format(unit) + dtype = f"m8[{unit}]" arr = np.array([[1, 2, 3]], dtype=dtype) df = DataFrame(arr) result = df.astype(dtype) @@ -880,7 +561,7 @@ def test_astype_to_timedelta_unit_ns(self, unit): def test_astype_to_timedelta_unit(self, unit): # coerce to float # gh-19223 - dtype = "m8[{}]".format(unit) + dtype = f"m8[{unit}]" arr = np.array([[1, 2, 3]], dtype=dtype) df = DataFrame(arr) result = df.astype(dtype) @@ -892,21 +573,21 @@ def test_astype_to_timedelta_unit(self, unit): def test_astype_to_incorrect_datetimelike(self, unit): # trying to astype a m to a M, or vice-versa # gh-19224 - dtype = "M8[{}]".format(unit) - other = "m8[{}]".format(unit) + dtype = f"M8[{unit}]" + other = f"m8[{unit}]" df = DataFrame(np.array([[1, 2, 3]], dtype=dtype)) msg = ( - r"cannot astype a datetimelike from \[datetime64\[ns\]\] to " - r"\[timedelta64\[{}\]\]" - ).format(unit) + fr"cannot astype a datetimelike from \[datetime64\[ns\]\] to " + fr"\[timedelta64\[{unit}\]\]" + ) with pytest.raises(TypeError, match=msg): df.astype(other) msg = ( - r"cannot astype a timedelta from \[timedelta64\[ns\]\] to " - r"\[datetime64\[{}\]\]" - ).format(unit) + fr"cannot astype a timedelta from \[timedelta64\[ns\]\] to " + fr"\[datetime64\[{unit}\]\]" + ) df = DataFrame(np.array([[1, 2, 3]], dtype=other)) with pytest.raises(TypeError, match=msg): df.astype(dtype) diff --git a/pandas/tests/frame/test_join.py b/pandas/tests/frame/test_join.py index c6e28f3c64f12..8c388a887158f 100644 --- a/pandas/tests/frame/test_join.py +++ b/pandas/tests/frame/test_join.py @@ -161,7 +161,7 @@ def test_join_overlap(float_frame): def test_join_period_index(frame_with_period_index): - other = frame_with_period_index.rename(columns=lambda x: "{key}{key}".format(key=x)) + other = frame_with_period_index.rename(columns=lambda key: f"{key}{key}") joined_values = np.concatenate([frame_with_period_index.values] * 2, axis=1) diff --git a/pandas/tests/frame/test_missing.py b/pandas/tests/frame/test_missing.py index ae0516dd29a1f..196df8ba00476 100644 --- a/pandas/tests/frame/test_missing.py +++ b/pandas/tests/frame/test_missing.py @@ -4,8 +4,6 @@ import numpy as np import pytest -import pandas.util._test_decorators as td - import pandas as pd from pandas import Categorical, DataFrame, Series, Timestamp, date_range import pandas._testing as tm @@ -705,281 +703,3 @@ def test_fill_value_when_combine_const(self): exp = df.fillna(0).add(2) res = df.add(2, fill_value=0) tm.assert_frame_equal(res, exp) - - -class TestDataFrameInterpolate: - def test_interp_basic(self): - df = DataFrame( - { - "A": [1, 2, np.nan, 4], - "B": [1, 4, 9, np.nan], - "C": [1, 2, 3, 5], - "D": list("abcd"), - } - ) - expected = DataFrame( - { - "A": [1.0, 2.0, 3.0, 4.0], - "B": [1.0, 4.0, 9.0, 9.0], - "C": [1, 2, 3, 5], - "D": list("abcd"), - } - ) - result = df.interpolate() - tm.assert_frame_equal(result, expected) - - result = df.set_index("C").interpolate() - expected = df.set_index("C") - expected.loc[3, "A"] = 3 - expected.loc[5, "B"] = 9 - tm.assert_frame_equal(result, expected) - - def test_interp_bad_method(self): - df = DataFrame( - { - "A": [1, 2, np.nan, 4], - "B": [1, 4, 9, np.nan], - "C": [1, 2, 3, 5], - "D": list("abcd"), - } - ) - with pytest.raises(ValueError): - df.interpolate(method="not_a_method") - - def test_interp_combo(self): - df = DataFrame( - { - "A": [1.0, 2.0, np.nan, 4.0], - "B": [1, 4, 9, np.nan], - "C": [1, 2, 3, 5], - "D": list("abcd"), - } - ) - - result = df["A"].interpolate() - expected = Series([1.0, 2.0, 3.0, 4.0], name="A") - tm.assert_series_equal(result, expected) - - result = df["A"].interpolate(downcast="infer") - expected = Series([1, 2, 3, 4], name="A") - tm.assert_series_equal(result, expected) - - def test_interp_nan_idx(self): - df = DataFrame({"A": [1, 2, np.nan, 4], "B": [np.nan, 2, 3, 4]}) - df = df.set_index("A") - with pytest.raises(NotImplementedError): - df.interpolate(method="values") - - @td.skip_if_no_scipy - def test_interp_various(self): - df = DataFrame( - {"A": [1, 2, np.nan, 4, 5, np.nan, 7], "C": [1, 2, 3, 5, 8, 13, 21]} - ) - df = df.set_index("C") - expected = df.copy() - result = df.interpolate(method="polynomial", order=1) - - expected.A.loc[3] = 2.66666667 - expected.A.loc[13] = 5.76923076 - tm.assert_frame_equal(result, expected) - - result = df.interpolate(method="cubic") - # GH #15662. - expected.A.loc[3] = 2.81547781 - expected.A.loc[13] = 5.52964175 - tm.assert_frame_equal(result, expected) - - result = df.interpolate(method="nearest") - expected.A.loc[3] = 2 - expected.A.loc[13] = 5 - tm.assert_frame_equal(result, expected, check_dtype=False) - - result = df.interpolate(method="quadratic") - expected.A.loc[3] = 2.82150771 - expected.A.loc[13] = 6.12648668 - tm.assert_frame_equal(result, expected) - - result = df.interpolate(method="slinear") - expected.A.loc[3] = 2.66666667 - expected.A.loc[13] = 5.76923077 - tm.assert_frame_equal(result, expected) - - result = df.interpolate(method="zero") - expected.A.loc[3] = 2.0 - expected.A.loc[13] = 5 - tm.assert_frame_equal(result, expected, check_dtype=False) - - @td.skip_if_no_scipy - def test_interp_alt_scipy(self): - df = DataFrame( - {"A": [1, 2, np.nan, 4, 5, np.nan, 7], "C": [1, 2, 3, 5, 8, 13, 21]} - ) - result = df.interpolate(method="barycentric") - expected = df.copy() - expected.loc[2, "A"] = 3 - expected.loc[5, "A"] = 6 - tm.assert_frame_equal(result, expected) - - result = df.interpolate(method="barycentric", downcast="infer") - tm.assert_frame_equal(result, expected.astype(np.int64)) - - result = df.interpolate(method="krogh") - expectedk = df.copy() - expectedk["A"] = expected["A"] - tm.assert_frame_equal(result, expectedk) - - result = df.interpolate(method="pchip") - expected.loc[2, "A"] = 3 - expected.loc[5, "A"] = 6.0 - - tm.assert_frame_equal(result, expected) - - def test_interp_rowwise(self): - df = DataFrame( - { - 0: [1, 2, np.nan, 4], - 1: [2, 3, 4, np.nan], - 2: [np.nan, 4, 5, 6], - 3: [4, np.nan, 6, 7], - 4: [1, 2, 3, 4], - } - ) - result = df.interpolate(axis=1) - expected = df.copy() - expected.loc[3, 1] = 5 - expected.loc[0, 2] = 3 - expected.loc[1, 3] = 3 - expected[4] = expected[4].astype(np.float64) - tm.assert_frame_equal(result, expected) - - result = df.interpolate(axis=1, method="values") - tm.assert_frame_equal(result, expected) - - result = df.interpolate(axis=0) - expected = df.interpolate() - tm.assert_frame_equal(result, expected) - - @pytest.mark.parametrize( - "axis_name, axis_number", - [ - pytest.param("rows", 0, id="rows_0"), - pytest.param("index", 0, id="index_0"), - pytest.param("columns", 1, id="columns_1"), - ], - ) - def test_interp_axis_names(self, axis_name, axis_number): - # GH 29132: test axis names - data = {0: [0, np.nan, 6], 1: [1, np.nan, 7], 2: [2, 5, 8]} - - df = DataFrame(data, dtype=np.float64) - result = df.interpolate(axis=axis_name, method="linear") - expected = df.interpolate(axis=axis_number, method="linear") - tm.assert_frame_equal(result, expected) - - def test_rowwise_alt(self): - df = DataFrame( - { - 0: [0, 0.5, 1.0, np.nan, 4, 8, np.nan, np.nan, 64], - 1: [1, 2, 3, 4, 3, 2, 1, 0, -1], - } - ) - df.interpolate(axis=0) - - @pytest.mark.parametrize( - "check_scipy", [False, pytest.param(True, marks=td.skip_if_no_scipy)] - ) - def test_interp_leading_nans(self, check_scipy): - df = DataFrame( - {"A": [np.nan, np.nan, 0.5, 0.25, 0], "B": [np.nan, -3, -3.5, np.nan, -4]} - ) - result = df.interpolate() - expected = df.copy() - expected["B"].loc[3] = -3.75 - tm.assert_frame_equal(result, expected) - - if check_scipy: - result = df.interpolate(method="polynomial", order=1) - tm.assert_frame_equal(result, expected) - - def test_interp_raise_on_only_mixed(self): - df = DataFrame( - { - "A": [1, 2, np.nan, 4], - "B": ["a", "b", "c", "d"], - "C": [np.nan, 2, 5, 7], - "D": [np.nan, np.nan, 9, 9], - "E": [1, 2, 3, 4], - } - ) - with pytest.raises(TypeError): - df.interpolate(axis=1) - - def test_interp_raise_on_all_object_dtype(self): - # GH 22985 - df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, dtype="object") - msg = ( - "Cannot interpolate with all object-dtype columns " - "in the DataFrame. Try setting at least one " - "column to a numeric dtype." - ) - with pytest.raises(TypeError, match=msg): - df.interpolate() - - def test_interp_inplace(self): - df = DataFrame({"a": [1.0, 2.0, np.nan, 4.0]}) - expected = DataFrame({"a": [1.0, 2.0, 3.0, 4.0]}) - result = df.copy() - result["a"].interpolate(inplace=True) - tm.assert_frame_equal(result, expected) - - result = df.copy() - result["a"].interpolate(inplace=True, downcast="infer") - tm.assert_frame_equal(result, expected.astype("int64")) - - def test_interp_inplace_row(self): - # GH 10395 - result = DataFrame( - {"a": [1.0, 2.0, 3.0, 4.0], "b": [np.nan, 2.0, 3.0, 4.0], "c": [3, 2, 2, 2]} - ) - expected = result.interpolate(method="linear", axis=1, inplace=False) - result.interpolate(method="linear", axis=1, inplace=True) - tm.assert_frame_equal(result, expected) - - def test_interp_ignore_all_good(self): - # GH - df = DataFrame( - { - "A": [1, 2, np.nan, 4], - "B": [1, 2, 3, 4], - "C": [1.0, 2.0, np.nan, 4.0], - "D": [1.0, 2.0, 3.0, 4.0], - } - ) - expected = DataFrame( - { - "A": np.array([1, 2, 3, 4], dtype="float64"), - "B": np.array([1, 2, 3, 4], dtype="int64"), - "C": np.array([1.0, 2.0, 3, 4.0], dtype="float64"), - "D": np.array([1.0, 2.0, 3.0, 4.0], dtype="float64"), - } - ) - - result = df.interpolate(downcast=None) - tm.assert_frame_equal(result, expected) - - # all good - result = df[["B", "D"]].interpolate(downcast=None) - tm.assert_frame_equal(result, df[["B", "D"]]) - - @pytest.mark.parametrize("axis", [0, 1]) - def test_interp_time_inplace_axis(self, axis): - # GH 9687 - periods = 5 - idx = pd.date_range(start="2014-01-01", periods=periods) - data = np.random.rand(periods, periods) - data[data < 0.5] = np.nan - expected = pd.DataFrame(index=idx, columns=idx, data=data) - - result = expected.interpolate(axis=0, method="time") - expected.interpolate(axis=0, method="time", inplace=True) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/test_mutate_columns.py b/pandas/tests/frame/test_mutate_columns.py index 8bc2aa214e035..33f71602f4713 100644 --- a/pandas/tests/frame/test_mutate_columns.py +++ b/pandas/tests/frame/test_mutate_columns.py @@ -10,82 +10,6 @@ class TestDataFrameMutateColumns: - def test_assign(self): - df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) - original = df.copy() - result = df.assign(C=df.B / df.A) - expected = df.copy() - expected["C"] = [4, 2.5, 2] - tm.assert_frame_equal(result, expected) - - # lambda syntax - result = df.assign(C=lambda x: x.B / x.A) - tm.assert_frame_equal(result, expected) - - # original is unmodified - tm.assert_frame_equal(df, original) - - # Non-Series array-like - result = df.assign(C=[4, 2.5, 2]) - tm.assert_frame_equal(result, expected) - # original is unmodified - tm.assert_frame_equal(df, original) - - result = df.assign(B=df.B / df.A) - expected = expected.drop("B", axis=1).rename(columns={"C": "B"}) - tm.assert_frame_equal(result, expected) - - # overwrite - result = df.assign(A=df.A + df.B) - expected = df.copy() - expected["A"] = [5, 7, 9] - tm.assert_frame_equal(result, expected) - - # lambda - result = df.assign(A=lambda x: x.A + x.B) - tm.assert_frame_equal(result, expected) - - def test_assign_multiple(self): - df = DataFrame([[1, 4], [2, 5], [3, 6]], columns=["A", "B"]) - result = df.assign(C=[7, 8, 9], D=df.A, E=lambda x: x.B) - expected = DataFrame( - [[1, 4, 7, 1, 4], [2, 5, 8, 2, 5], [3, 6, 9, 3, 6]], columns=list("ABCDE") - ) - tm.assert_frame_equal(result, expected) - - def test_assign_order(self): - # GH 9818 - df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) - result = df.assign(D=df.A + df.B, C=df.A - df.B) - - expected = DataFrame([[1, 2, 3, -1], [3, 4, 7, -1]], columns=list("ABDC")) - tm.assert_frame_equal(result, expected) - result = df.assign(C=df.A - df.B, D=df.A + df.B) - - expected = DataFrame([[1, 2, -1, 3], [3, 4, -1, 7]], columns=list("ABCD")) - - tm.assert_frame_equal(result, expected) - - def test_assign_bad(self): - df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) - - # non-keyword argument - with pytest.raises(TypeError): - df.assign(lambda x: x.A) - with pytest.raises(AttributeError): - df.assign(C=df.A, D=df.A + df.C) - - def test_assign_dependent(self): - df = DataFrame({"A": [1, 2], "B": [3, 4]}) - - result = df.assign(C=df.A, D=lambda x: x["A"] + x["C"]) - expected = DataFrame([[1, 3, 1, 2], [2, 4, 2, 4]], columns=list("ABCD")) - tm.assert_frame_equal(result, expected) - - result = df.assign(C=lambda df: df.A, D=lambda df: df["A"] + df["C"]) - expected = DataFrame([[1, 3, 1, 2], [2, 4, 2, 4]], columns=list("ABCD")) - tm.assert_frame_equal(result, expected) - def test_insert_error_msmgs(self): # GH 7432 diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 162f3c114fa5d..542d9835bb5d3 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -685,25 +685,6 @@ def test_boolean_comparison(self): with pytest.raises(ValueError, match=msg1d): result = df == tup - def test_combine_generic(self, float_frame): - df1 = float_frame - df2 = float_frame.loc[float_frame.index[:-5], ["A", "B", "C"]] - - combined = df1.combine(df2, np.add) - combined2 = df2.combine(df1, np.add) - assert combined["D"].isna().all() - assert combined2["D"].isna().all() - - chunk = combined.loc[combined.index[:-5], ["A", "B", "C"]] - chunk2 = combined2.loc[combined2.index[:-5], ["A", "B", "C"]] - - exp = ( - float_frame.loc[float_frame.index[:-5], ["A", "B", "C"]].reindex_like(chunk) - * 2 - ) - tm.assert_frame_equal(chunk, exp) - tm.assert_frame_equal(chunk2, exp) - def test_inplace_ops_alignment(self): # inplace ops / ops alignment @@ -840,8 +821,8 @@ def test_inplace_ops_identity2(self, op): df["a"] = [True, False, True] df_copy = df.copy() - iop = "__i{}__".format(op) - op = "__{}__".format(op) + iop = f"__i{op}__" + op = f"__{op}__" # no id change and value is correct getattr(df, iop)(operand) diff --git a/pandas/tests/frame/test_period.py b/pandas/tests/frame/test_period.py index a6b2b334d3ec8..c378194b9e2b2 100644 --- a/pandas/tests/frame/test_period.py +++ b/pandas/tests/frame/test_period.py @@ -1,26 +1,9 @@ -from datetime import timedelta - import numpy as np -import pytest -import pandas as pd -from pandas import ( - DataFrame, - DatetimeIndex, - Index, - PeriodIndex, - Timedelta, - date_range, - period_range, - to_datetime, -) +from pandas import DataFrame, Index, PeriodIndex, period_range import pandas._testing as tm -def _permute(obj): - return obj.take(np.random.permutation(len(obj))) - - class TestPeriodIndex: def test_as_frame_columns(self): rng = period_range("1/1/2000", periods=5) @@ -49,108 +32,9 @@ def test_frame_setitem(self): assert isinstance(rs.index, PeriodIndex) tm.assert_index_equal(rs.index, rng) - def test_frame_to_time_stamp(self): - K = 5 - index = period_range(freq="A", start="1/1/2001", end="12/1/2009") - df = DataFrame(np.random.randn(len(index), K), index=index) - df["mix"] = "a" - - exp_index = date_range("1/1/2001", end="12/31/2009", freq="A-DEC") - exp_index = exp_index + Timedelta(1, "D") - Timedelta(1, "ns") - result = df.to_timestamp("D", "end") - tm.assert_index_equal(result.index, exp_index) - tm.assert_numpy_array_equal(result.values, df.values) - - exp_index = date_range("1/1/2001", end="1/1/2009", freq="AS-JAN") - result = df.to_timestamp("D", "start") - tm.assert_index_equal(result.index, exp_index) - - def _get_with_delta(delta, freq="A-DEC"): - return date_range( - to_datetime("1/1/2001") + delta, - to_datetime("12/31/2009") + delta, - freq=freq, - ) - - delta = timedelta(hours=23) - result = df.to_timestamp("H", "end") - exp_index = _get_with_delta(delta) - exp_index = exp_index + Timedelta(1, "h") - Timedelta(1, "ns") - tm.assert_index_equal(result.index, exp_index) - - delta = timedelta(hours=23, minutes=59) - result = df.to_timestamp("T", "end") - exp_index = _get_with_delta(delta) - exp_index = exp_index + Timedelta(1, "m") - Timedelta(1, "ns") - tm.assert_index_equal(result.index, exp_index) - - result = df.to_timestamp("S", "end") - delta = timedelta(hours=23, minutes=59, seconds=59) - exp_index = _get_with_delta(delta) - exp_index = exp_index + Timedelta(1, "s") - Timedelta(1, "ns") - tm.assert_index_equal(result.index, exp_index) - - # columns - df = df.T - - exp_index = date_range("1/1/2001", end="12/31/2009", freq="A-DEC") - exp_index = exp_index + Timedelta(1, "D") - Timedelta(1, "ns") - result = df.to_timestamp("D", "end", axis=1) - tm.assert_index_equal(result.columns, exp_index) - tm.assert_numpy_array_equal(result.values, df.values) - - exp_index = date_range("1/1/2001", end="1/1/2009", freq="AS-JAN") - result = df.to_timestamp("D", "start", axis=1) - tm.assert_index_equal(result.columns, exp_index) - - delta = timedelta(hours=23) - result = df.to_timestamp("H", "end", axis=1) - exp_index = _get_with_delta(delta) - exp_index = exp_index + Timedelta(1, "h") - Timedelta(1, "ns") - tm.assert_index_equal(result.columns, exp_index) - - delta = timedelta(hours=23, minutes=59) - result = df.to_timestamp("T", "end", axis=1) - exp_index = _get_with_delta(delta) - exp_index = exp_index + Timedelta(1, "m") - Timedelta(1, "ns") - tm.assert_index_equal(result.columns, exp_index) - - result = df.to_timestamp("S", "end", axis=1) - delta = timedelta(hours=23, minutes=59, seconds=59) - exp_index = _get_with_delta(delta) - exp_index = exp_index + Timedelta(1, "s") - Timedelta(1, "ns") - tm.assert_index_equal(result.columns, exp_index) - - # invalid axis - with pytest.raises(ValueError, match="axis"): - df.to_timestamp(axis=2) - - result1 = df.to_timestamp("5t", axis=1) - result2 = df.to_timestamp("t", axis=1) - expected = pd.date_range("2001-01-01", "2009-01-01", freq="AS") - assert isinstance(result1.columns, DatetimeIndex) - assert isinstance(result2.columns, DatetimeIndex) - tm.assert_numpy_array_equal(result1.columns.asi8, expected.asi8) - tm.assert_numpy_array_equal(result2.columns.asi8, expected.asi8) - # PeriodIndex.to_timestamp always use 'infer' - assert result1.columns.freqstr == "AS-JAN" - assert result2.columns.freqstr == "AS-JAN" - def test_frame_index_to_string(self): index = PeriodIndex(["2011-1", "2011-2", "2011-3"], freq="M") frame = DataFrame(np.random.randn(3, 4), index=index) # it works! frame.to_string() - - def test_align_frame(self): - rng = period_range("1/1/2000", "1/1/2010", freq="A") - ts = DataFrame(np.random.randn(len(rng), 3), index=rng) - - result = ts + ts[::2] - expected = ts + ts - expected.values[1::2] = np.nan - tm.assert_frame_equal(result, expected) - - result = ts + _permute(ts[::2]) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/test_query_eval.py b/pandas/tests/frame/test_query_eval.py index 703e05998e93c..bf9eeb532b43b 100644 --- a/pandas/tests/frame/test_query_eval.py +++ b/pandas/tests/frame/test_query_eval.py @@ -78,45 +78,48 @@ def test_query_numexpr(self): class TestDataFrameEval: - def test_ops(self): + + # smaller hits python, larger hits numexpr + @pytest.mark.parametrize("n", [4, 4000]) + @pytest.mark.parametrize( + "op_str,op,rop", + [ + ("+", "__add__", "__radd__"), + ("-", "__sub__", "__rsub__"), + ("*", "__mul__", "__rmul__"), + ("/", "__truediv__", "__rtruediv__"), + ], + ) + def test_ops(self, op_str, op, rop, n): # tst ops and reversed ops in evaluation # GH7198 - # smaller hits python, larger hits numexpr - for n in [4, 4000]: - - df = DataFrame(1, index=range(n), columns=list("abcd")) - df.iloc[0] = 2 - m = df.mean() + df = DataFrame(1, index=range(n), columns=list("abcd")) + df.iloc[0] = 2 + m = df.mean() - for op_str, op, rop in [ - ("+", "__add__", "__radd__"), - ("-", "__sub__", "__rsub__"), - ("*", "__mul__", "__rmul__"), - ("/", "__truediv__", "__rtruediv__"), - ]: - - base = DataFrame( # noqa - np.tile(m.values, n).reshape(n, -1), columns=list("abcd") - ) + base = DataFrame( # noqa + np.tile(m.values, n).reshape(n, -1), columns=list("abcd") + ) - expected = eval("base{op}df".format(op=op_str)) + expected = eval(f"base {op_str} df") - # ops as strings - result = eval("m{op}df".format(op=op_str)) - tm.assert_frame_equal(result, expected) + # ops as strings + result = eval(f"m {op_str} df") + tm.assert_frame_equal(result, expected) - # these are commutative - if op in ["+", "*"]: - result = getattr(df, op)(m) - tm.assert_frame_equal(result, expected) + # these are commutative + if op in ["+", "*"]: + result = getattr(df, op)(m) + tm.assert_frame_equal(result, expected) - # these are not - elif op in ["-", "/"]: - result = getattr(df, rop)(m) - tm.assert_frame_equal(result, expected) + # these are not + elif op in ["-", "/"]: + result = getattr(df, rop)(m) + tm.assert_frame_equal(result, expected) + def test_dataframe_sub_numexpr_path(self): # GH7192: Note we need a large number of rows to ensure this # goes through the numexpr path df = DataFrame(dict(A=np.random.randn(25000))) @@ -451,9 +454,7 @@ def test_date_query_with_non_date(self): for op in ["<", ">", "<=", ">="]: with pytest.raises(TypeError): - df.query( - "dates {op} nondate".format(op=op), parser=parser, engine=engine - ) + df.query(f"dates {op} nondate", parser=parser, engine=engine) def test_query_syntax_error(self): engine, parser = self.engine, self.parser @@ -687,10 +688,9 @@ def test_inf(self): n = 10 df = DataFrame({"a": np.random.rand(n), "b": np.random.rand(n)}) df.loc[::2, 0] = np.inf - ops = "==", "!=" - d = dict(zip(ops, (operator.eq, operator.ne))) + d = {"==": operator.eq, "!=": operator.ne} for op, f in d.items(): - q = "a {op} inf".format(op=op) + q = f"a {op} inf" expected = df[f(df.a, np.inf)] result = df.query(q, engine=self.engine, parser=self.parser) tm.assert_frame_equal(result, expected) @@ -854,7 +854,7 @@ def test_str_query_method(self, parser, engine): ops = 2 * ([eq] + [ne]) for lhs, op, rhs in zip(lhs, ops, rhs): - ex = "{lhs} {op} {rhs}".format(lhs=lhs, op=op, rhs=rhs) + ex = f"{lhs} {op} {rhs}" msg = r"'(Not)?In' nodes are not implemented" with pytest.raises(NotImplementedError, match=msg): df.query( @@ -895,7 +895,7 @@ def test_str_list_query_method(self, parser, engine): ops = 2 * ([eq] + [ne]) for lhs, op, rhs in zip(lhs, ops, rhs): - ex = "{lhs} {op} {rhs}".format(lhs=lhs, op=op, rhs=rhs) + ex = f"{lhs} {op} {rhs}" with pytest.raises(NotImplementedError): df.query(ex, engine=engine, parser=parser) else: @@ -1042,7 +1042,7 @@ def test_invalid_type_for_operator_raises(self, parser, engine, op): msg = r"unsupported operand type\(s\) for .+: '.+' and '.+'" with pytest.raises(TypeError, match=msg): - df.eval("a {0} b".format(op), engine=engine, parser=parser) + df.eval(f"a {op} b", engine=engine, parser=parser) class TestDataFrameQueryBacktickQuoting: diff --git a/pandas/tests/frame/test_repr_info.py b/pandas/tests/frame/test_repr_info.py index a7e01d8f1fd6d..c5d4d59adbc35 100644 --- a/pandas/tests/frame/test_repr_info.py +++ b/pandas/tests/frame/test_repr_info.py @@ -1,16 +1,10 @@ from datetime import datetime, timedelta from io import StringIO -import re -import sys -import textwrap import warnings import numpy as np import pytest -from pandas.compat import PYPY - -import pandas as pd from pandas import ( Categorical, DataFrame, @@ -129,9 +123,6 @@ def test_repr_unsortable(self, float_frame): def test_repr_unicode(self): uval = "\u03c3\u03c3\u03c3\u03c3" - # TODO(wesm): is this supposed to be used? - bval = uval.encode("utf-8") # noqa - df = DataFrame({"A": [uval, uval]}) result = repr(df) @@ -195,357 +186,6 @@ def test_latex_repr(self): # GH 12182 assert df._repr_latex_() is None - def test_info(self, float_frame, datetime_frame): - io = StringIO() - float_frame.info(buf=io) - datetime_frame.info(buf=io) - - frame = DataFrame(np.random.randn(5, 3)) - - frame.info() - frame.info(verbose=False) - - def test_info_verbose(self): - buf = StringIO() - size = 1001 - start = 5 - frame = DataFrame(np.random.randn(3, size)) - frame.info(verbose=True, buf=buf) - - res = buf.getvalue() - header = " # Column Dtype \n--- ------ ----- " - assert header in res - - frame.info(verbose=True, buf=buf) - buf.seek(0) - lines = buf.readlines() - assert len(lines) > 0 - - for i, line in enumerate(lines): - if i >= start and i < start + size: - line_nr = f" {i - start} " - assert line.startswith(line_nr) - - def test_info_memory(self): - # https://github.com/pandas-dev/pandas/issues/21056 - df = pd.DataFrame({"a": pd.Series([1, 2], dtype="i8")}) - buf = StringIO() - df.info(buf=buf) - result = buf.getvalue() - bytes = float(df.memory_usage().sum()) - - expected = textwrap.dedent( - f"""\ - - RangeIndex: 2 entries, 0 to 1 - Data columns (total 1 columns): - # Column Non-Null Count Dtype - --- ------ -------------- ----- - 0 a 2 non-null int64 - dtypes: int64(1) - memory usage: {bytes} bytes - """ - ) - - assert result == expected - - def test_info_wide(self): - from pandas import set_option, reset_option - - io = StringIO() - df = DataFrame(np.random.randn(5, 101)) - df.info(buf=io) - - io = StringIO() - df.info(buf=io, max_cols=101) - rs = io.getvalue() - assert len(rs.splitlines()) > 100 - xp = rs - - set_option("display.max_info_columns", 101) - io = StringIO() - df.info(buf=io) - assert rs == xp - reset_option("display.max_info_columns") - - def test_info_duplicate_columns(self): - io = StringIO() - - # it works! - frame = DataFrame(np.random.randn(1500, 4), columns=["a", "a", "b", "b"]) - frame.info(buf=io) - - def test_info_duplicate_columns_shows_correct_dtypes(self): - # GH11761 - io = StringIO() - - frame = DataFrame([[1, 2.0]], columns=["a", "a"]) - frame.info(buf=io) - io.seek(0) - lines = io.readlines() - assert " 0 a 1 non-null int64 \n" == lines[5] - assert " 1 a 1 non-null float64\n" == lines[6] - - def test_info_shows_column_dtypes(self): - dtypes = [ - "int64", - "float64", - "datetime64[ns]", - "timedelta64[ns]", - "complex128", - "object", - "bool", - ] - data = {} - n = 10 - for i, dtype in enumerate(dtypes): - data[i] = np.random.randint(2, size=n).astype(dtype) - df = DataFrame(data) - buf = StringIO() - df.info(buf=buf) - res = buf.getvalue() - header = ( - " # Column Non-Null Count Dtype \n" - "--- ------ -------------- ----- " - ) - assert header in res - for i, dtype in enumerate(dtypes): - name = f" {i:d} {i:d} {n:d} non-null {dtype}" - assert name in res - - def test_info_max_cols(self): - df = DataFrame(np.random.randn(10, 5)) - for len_, verbose in [(5, None), (5, False), (12, True)]: - # For verbose always ^ setting ^ summarize ^ full output - with option_context("max_info_columns", 4): - buf = StringIO() - df.info(buf=buf, verbose=verbose) - res = buf.getvalue() - assert len(res.strip().split("\n")) == len_ - - for len_, verbose in [(12, None), (5, False), (12, True)]: - - # max_cols not exceeded - with option_context("max_info_columns", 5): - buf = StringIO() - df.info(buf=buf, verbose=verbose) - res = buf.getvalue() - assert len(res.strip().split("\n")) == len_ - - for len_, max_cols in [(12, 5), (5, 4)]: - # setting truncates - with option_context("max_info_columns", 4): - buf = StringIO() - df.info(buf=buf, max_cols=max_cols) - res = buf.getvalue() - assert len(res.strip().split("\n")) == len_ - - # setting wouldn't truncate - with option_context("max_info_columns", 5): - buf = StringIO() - df.info(buf=buf, max_cols=max_cols) - res = buf.getvalue() - assert len(res.strip().split("\n")) == len_ - - def test_info_memory_usage(self): - # Ensure memory usage is displayed, when asserted, on the last line - dtypes = [ - "int64", - "float64", - "datetime64[ns]", - "timedelta64[ns]", - "complex128", - "object", - "bool", - ] - data = {} - n = 10 - for i, dtype in enumerate(dtypes): - data[i] = np.random.randint(2, size=n).astype(dtype) - df = DataFrame(data) - buf = StringIO() - - # display memory usage case - df.info(buf=buf, memory_usage=True) - res = buf.getvalue().splitlines() - assert "memory usage: " in res[-1] - - # do not display memory usage case - df.info(buf=buf, memory_usage=False) - res = buf.getvalue().splitlines() - assert "memory usage: " not in res[-1] - - df.info(buf=buf, memory_usage=True) - res = buf.getvalue().splitlines() - - # memory usage is a lower bound, so print it as XYZ+ MB - assert re.match(r"memory usage: [^+]+\+", res[-1]) - - df.iloc[:, :5].info(buf=buf, memory_usage=True) - res = buf.getvalue().splitlines() - - # excluded column with object dtype, so estimate is accurate - assert not re.match(r"memory usage: [^+]+\+", res[-1]) - - # Test a DataFrame with duplicate columns - dtypes = ["int64", "int64", "int64", "float64"] - data = {} - n = 100 - for i, dtype in enumerate(dtypes): - data[i] = np.random.randint(2, size=n).astype(dtype) - df = DataFrame(data) - df.columns = dtypes - - df_with_object_index = pd.DataFrame({"a": [1]}, index=["foo"]) - df_with_object_index.info(buf=buf, memory_usage=True) - res = buf.getvalue().splitlines() - assert re.match(r"memory usage: [^+]+\+", res[-1]) - - df_with_object_index.info(buf=buf, memory_usage="deep") - res = buf.getvalue().splitlines() - assert re.match(r"memory usage: [^+]+$", res[-1]) - - # Ensure df size is as expected - # (cols * rows * bytes) + index size - df_size = df.memory_usage().sum() - exp_size = len(dtypes) * n * 8 + df.index.nbytes - assert df_size == exp_size - - # Ensure number of cols in memory_usage is the same as df - size_df = np.size(df.columns.values) + 1 # index=True; default - assert size_df == np.size(df.memory_usage()) - - # assert deep works only on object - assert df.memory_usage().sum() == df.memory_usage(deep=True).sum() - - # test for validity - DataFrame(1, index=["a"], columns=["A"]).memory_usage(index=True) - DataFrame(1, index=["a"], columns=["A"]).index.nbytes - df = DataFrame( - data=1, - index=pd.MultiIndex.from_product([["a"], range(1000)]), - columns=["A"], - ) - df.index.nbytes - df.memory_usage(index=True) - df.index.values.nbytes - - mem = df.memory_usage(deep=True).sum() - assert mem > 0 - - @pytest.mark.skipif(PYPY, reason="on PyPy deep=True doesn't change result") - def test_info_memory_usage_deep_not_pypy(self): - df_with_object_index = pd.DataFrame({"a": [1]}, index=["foo"]) - assert ( - df_with_object_index.memory_usage(index=True, deep=True).sum() - > df_with_object_index.memory_usage(index=True).sum() - ) - - df_object = pd.DataFrame({"a": ["a"]}) - assert df_object.memory_usage(deep=True).sum() > df_object.memory_usage().sum() - - @pytest.mark.skipif(not PYPY, reason="on PyPy deep=True does not change result") - def test_info_memory_usage_deep_pypy(self): - df_with_object_index = pd.DataFrame({"a": [1]}, index=["foo"]) - assert ( - df_with_object_index.memory_usage(index=True, deep=True).sum() - == df_with_object_index.memory_usage(index=True).sum() - ) - - df_object = pd.DataFrame({"a": ["a"]}) - assert df_object.memory_usage(deep=True).sum() == df_object.memory_usage().sum() - - @pytest.mark.skipif(PYPY, reason="PyPy getsizeof() fails by design") - def test_usage_via_getsizeof(self): - df = DataFrame( - data=1, - index=pd.MultiIndex.from_product([["a"], range(1000)]), - columns=["A"], - ) - mem = df.memory_usage(deep=True).sum() - # sys.getsizeof will call the .memory_usage with - # deep=True, and add on some GC overhead - diff = mem - sys.getsizeof(df) - assert abs(diff) < 100 - - def test_info_memory_usage_qualified(self): - - buf = StringIO() - df = DataFrame(1, columns=list("ab"), index=[1, 2, 3]) - df.info(buf=buf) - assert "+" not in buf.getvalue() - - buf = StringIO() - df = DataFrame(1, columns=list("ab"), index=list("ABC")) - df.info(buf=buf) - assert "+" in buf.getvalue() - - buf = StringIO() - df = DataFrame( - 1, - columns=list("ab"), - index=pd.MultiIndex.from_product([range(3), range(3)]), - ) - df.info(buf=buf) - assert "+" not in buf.getvalue() - - buf = StringIO() - df = DataFrame( - 1, - columns=list("ab"), - index=pd.MultiIndex.from_product([range(3), ["foo", "bar"]]), - ) - df.info(buf=buf) - assert "+" in buf.getvalue() - - def test_info_memory_usage_bug_on_multiindex(self): - # GH 14308 - # memory usage introspection should not materialize .values - - from string import ascii_uppercase as uppercase - - def memory_usage(f): - return f.memory_usage(deep=True).sum() - - N = 100 - M = len(uppercase) - index = pd.MultiIndex.from_product( - [list(uppercase), pd.date_range("20160101", periods=N)], - names=["id", "date"], - ) - df = DataFrame({"value": np.random.randn(N * M)}, index=index) - - unstacked = df.unstack("id") - assert df.values.nbytes == unstacked.values.nbytes - assert memory_usage(df) > memory_usage(unstacked) - - # high upper bound - assert memory_usage(unstacked) - memory_usage(df) < 2000 - - def test_info_categorical(self): - # GH14298 - idx = pd.CategoricalIndex(["a", "b"]) - df = pd.DataFrame(np.zeros((2, 2)), index=idx, columns=idx) - - buf = StringIO() - df.info(buf=buf) - - def test_info_categorical_column(self): - - # make sure it works - n = 2500 - df = DataFrame({"int64": np.random.randint(100, size=n)}) - df["category"] = Series( - np.array(list("abcdefghij")).take(np.random.randint(0, 10, size=n)) - ).astype("category") - df.isna() - buf = StringIO() - df.info(buf=buf) - - df2 = df[df["category"] == "d"] - buf = StringIO() - df2.info(buf=buf) - def test_repr_categorical_dates_periods(self): # normal DataFrame dt = date_range("2011-01-01 09:00", freq="H", periods=5, tz="US/Eastern") diff --git a/pandas/tests/frame/test_reshape.py b/pandas/tests/frame/test_reshape.py index b3af5a7b7317e..46a4a0a2af4ba 100644 --- a/pandas/tests/frame/test_reshape.py +++ b/pandas/tests/frame/test_reshape.py @@ -765,7 +765,9 @@ def test_unstack_unused_level(self, cols): tm.assert_frame_equal(result, expected) def test_unstack_nan_index(self): # GH7466 - cast = lambda val: "{0:1}".format("" if val != val else val) + def cast(val): + val_str = "" if val != val else val + return f"{val_str:1}" def verify(df): mk_list = lambda a: list(a) if isinstance(a, tuple) else [a] diff --git a/pandas/tests/frame/test_timeseries.py b/pandas/tests/frame/test_timeseries.py index e89f4ee07ea00..5956f73bb11f0 100644 --- a/pandas/tests/frame/test_timeseries.py +++ b/pandas/tests/frame/test_timeseries.py @@ -1,30 +1,10 @@ -from datetime import datetime, time -from itertools import product - import numpy as np import pytest -import pytz import pandas as pd -from pandas import ( - DataFrame, - DatetimeIndex, - Index, - MultiIndex, - Series, - date_range, - period_range, - to_datetime, -) +from pandas import DataFrame, Series, date_range, to_datetime import pandas._testing as tm -import pandas.tseries.offsets as offsets - - -@pytest.fixture(params=product([True, False], [True, False])) -def close_open_fixture(request): - return request.param - class TestDataFrameTimeSeriesMethods: def test_frame_ctor_datetime64_column(self): @@ -54,7 +34,7 @@ def test_frame_append_datetime64_col_other_units(self): ns_dtype = np.dtype("M8[ns]") for unit in units: - dtype = np.dtype("M8[{unit}]".format(unit=unit)) + dtype = np.dtype(f"M8[{unit}]") vals = np.arange(n, dtype=np.int64).view(dtype) df = DataFrame({"ints": np.arange(n)}, index=np.arange(n)) @@ -70,7 +50,7 @@ def test_frame_append_datetime64_col_other_units(self): df["dates"] = np.arange(n, dtype=np.int64).view(ns_dtype) for unit in units: - dtype = np.dtype("M8[{unit}]".format(unit=unit)) + dtype = np.dtype(f"M8[{unit}]") vals = np.arange(n, dtype=np.int64).view(dtype) tmp = df.copy() @@ -80,54 +60,6 @@ def test_frame_append_datetime64_col_other_units(self): assert (tmp["dates"].values == ex_vals).all() - def test_asfreq(self, datetime_frame): - offset_monthly = datetime_frame.asfreq(offsets.BMonthEnd()) - rule_monthly = datetime_frame.asfreq("BM") - - tm.assert_almost_equal(offset_monthly["A"], rule_monthly["A"]) - - filled = rule_monthly.asfreq("B", method="pad") # noqa - # TODO: actually check that this worked. - - # don't forget! - filled_dep = rule_monthly.asfreq("B", method="pad") # noqa - - # test does not blow up on length-0 DataFrame - zero_length = datetime_frame.reindex([]) - result = zero_length.asfreq("BM") - assert result is not zero_length - - def test_asfreq_datetimeindex(self): - df = DataFrame( - {"A": [1, 2, 3]}, - index=[datetime(2011, 11, 1), datetime(2011, 11, 2), datetime(2011, 11, 3)], - ) - df = df.asfreq("B") - assert isinstance(df.index, DatetimeIndex) - - ts = df["A"].asfreq("B") - assert isinstance(ts.index, DatetimeIndex) - - def test_asfreq_fillvalue(self): - # test for fill value during upsampling, related to issue 3715 - - # setup - rng = pd.date_range("1/1/2016", periods=10, freq="2S") - ts = pd.Series(np.arange(len(rng)), index=rng) - df = pd.DataFrame({"one": ts}) - - # insert pre-existing missing value - df.loc["2016-01-01 00:00:08", "one"] = None - - actual_df = df.asfreq(freq="1S", fill_value=9.0) - expected_df = df.asfreq(freq="1S").fillna(9.0) - expected_df.loc["2016-01-01 00:00:08", "one"] = None - tm.assert_frame_equal(expected_df, actual_df) - - expected_series = ts.asfreq(freq="1S").fillna(9.0) - actual_series = ts.asfreq(freq="1S", fill_value=9.0) - tm.assert_series_equal(expected_series, actual_series) - @pytest.mark.parametrize( "data,idx,expected_first,expected_last", [ @@ -187,235 +119,6 @@ def test_first_valid_index_all_nan(self, klass): assert obj.first_valid_index() is None assert obj.iloc[:0].first_valid_index() is None - def test_first_subset(self): - ts = tm.makeTimeDataFrame(freq="12h") - result = ts.first("10d") - assert len(result) == 20 - - ts = tm.makeTimeDataFrame(freq="D") - result = ts.first("10d") - assert len(result) == 10 - - result = ts.first("3M") - expected = ts[:"3/31/2000"] - tm.assert_frame_equal(result, expected) - - result = ts.first("21D") - expected = ts[:21] - tm.assert_frame_equal(result, expected) - - result = ts[:0].first("3M") - tm.assert_frame_equal(result, ts[:0]) - - def test_first_raises(self): - # GH20725 - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]]) - with pytest.raises(TypeError): # index is not a DatetimeIndex - df.first("1D") - - def test_last_subset(self): - ts = tm.makeTimeDataFrame(freq="12h") - result = ts.last("10d") - assert len(result) == 20 - - ts = tm.makeTimeDataFrame(nper=30, freq="D") - result = ts.last("10d") - assert len(result) == 10 - - result = ts.last("21D") - expected = ts["2000-01-10":] - tm.assert_frame_equal(result, expected) - - result = ts.last("21D") - expected = ts[-21:] - tm.assert_frame_equal(result, expected) - - result = ts[:0].last("3M") - tm.assert_frame_equal(result, ts[:0]) - - def test_last_raises(self): - # GH20725 - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]]) - with pytest.raises(TypeError): # index is not a DatetimeIndex - df.last("1D") - - def test_at_time(self): - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = DataFrame(np.random.randn(len(rng), 2), index=rng) - rs = ts.at_time(rng[1]) - assert (rs.index.hour == rng[1].hour).all() - assert (rs.index.minute == rng[1].minute).all() - assert (rs.index.second == rng[1].second).all() - - result = ts.at_time("9:30") - expected = ts.at_time(time(9, 30)) - tm.assert_frame_equal(result, expected) - - result = ts.loc[time(9, 30)] - expected = ts.loc[(rng.hour == 9) & (rng.minute == 30)] - - tm.assert_frame_equal(result, expected) - - # midnight, everything - rng = date_range("1/1/2000", "1/31/2000") - ts = DataFrame(np.random.randn(len(rng), 3), index=rng) - - result = ts.at_time(time(0, 0)) - tm.assert_frame_equal(result, ts) - - # time doesn't exist - rng = date_range("1/1/2012", freq="23Min", periods=384) - ts = DataFrame(np.random.randn(len(rng), 2), rng) - rs = ts.at_time("16:00") - assert len(rs) == 0 - - @pytest.mark.parametrize( - "hour", ["1:00", "1:00AM", time(1), time(1, tzinfo=pytz.UTC)] - ) - def test_at_time_errors(self, hour): - # GH 24043 - dti = pd.date_range("2018", periods=3, freq="H") - df = pd.DataFrame(list(range(len(dti))), index=dti) - if getattr(hour, "tzinfo", None) is None: - result = df.at_time(hour) - expected = df.iloc[1:2] - tm.assert_frame_equal(result, expected) - else: - with pytest.raises(ValueError, match="Index must be timezone"): - df.at_time(hour) - - def test_at_time_tz(self): - # GH 24043 - dti = pd.date_range("2018", periods=3, freq="H", tz="US/Pacific") - df = pd.DataFrame(list(range(len(dti))), index=dti) - result = df.at_time(time(4, tzinfo=pytz.timezone("US/Eastern"))) - expected = df.iloc[1:2] - tm.assert_frame_equal(result, expected) - - def test_at_time_raises(self): - # GH20725 - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]]) - with pytest.raises(TypeError): # index is not a DatetimeIndex - df.at_time("00:00") - - @pytest.mark.parametrize("axis", ["index", "columns", 0, 1]) - def test_at_time_axis(self, axis): - # issue 8839 - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = DataFrame(np.random.randn(len(rng), len(rng))) - ts.index, ts.columns = rng, rng - - indices = rng[(rng.hour == 9) & (rng.minute == 30) & (rng.second == 0)] - - if axis in ["index", 0]: - expected = ts.loc[indices, :] - elif axis in ["columns", 1]: - expected = ts.loc[:, indices] - - result = ts.at_time("9:30", axis=axis) - tm.assert_frame_equal(result, expected) - - def test_between_time(self, close_open_fixture): - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = DataFrame(np.random.randn(len(rng), 2), index=rng) - stime = time(0, 0) - etime = time(1, 0) - inc_start, inc_end = close_open_fixture - - filtered = ts.between_time(stime, etime, inc_start, inc_end) - exp_len = 13 * 4 + 1 - if not inc_start: - exp_len -= 5 - if not inc_end: - exp_len -= 4 - - assert len(filtered) == exp_len - for rs in filtered.index: - t = rs.time() - if inc_start: - assert t >= stime - else: - assert t > stime - - if inc_end: - assert t <= etime - else: - assert t < etime - - result = ts.between_time("00:00", "01:00") - expected = ts.between_time(stime, etime) - tm.assert_frame_equal(result, expected) - - # across midnight - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = DataFrame(np.random.randn(len(rng), 2), index=rng) - stime = time(22, 0) - etime = time(9, 0) - - filtered = ts.between_time(stime, etime, inc_start, inc_end) - exp_len = (12 * 11 + 1) * 4 + 1 - if not inc_start: - exp_len -= 4 - if not inc_end: - exp_len -= 4 - - assert len(filtered) == exp_len - for rs in filtered.index: - t = rs.time() - if inc_start: - assert (t >= stime) or (t <= etime) - else: - assert (t > stime) or (t <= etime) - - if inc_end: - assert (t <= etime) or (t >= stime) - else: - assert (t < etime) or (t >= stime) - - def test_between_time_raises(self): - # GH20725 - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]]) - with pytest.raises(TypeError): # index is not a DatetimeIndex - df.between_time(start_time="00:00", end_time="12:00") - - def test_between_time_axis(self, axis): - # issue 8839 - rng = date_range("1/1/2000", periods=100, freq="10min") - ts = DataFrame(np.random.randn(len(rng), len(rng))) - stime, etime = ("08:00:00", "09:00:00") - exp_len = 7 - - if axis in ["index", 0]: - ts.index = rng - assert len(ts.between_time(stime, etime)) == exp_len - assert len(ts.between_time(stime, etime, axis=0)) == exp_len - - if axis in ["columns", 1]: - ts.columns = rng - selected = ts.between_time(stime, etime, axis=1).columns - assert len(selected) == exp_len - - def test_between_time_axis_raises(self, axis): - # issue 8839 - rng = date_range("1/1/2000", periods=100, freq="10min") - mask = np.arange(0, len(rng)) - rand_data = np.random.randn(len(rng), len(rng)) - ts = DataFrame(rand_data, index=rng, columns=rng) - stime, etime = ("08:00:00", "09:00:00") - - msg = "Index must be DatetimeIndex" - if axis in ["columns", 1]: - ts.index = mask - with pytest.raises(TypeError, match=msg): - ts.between_time(stime, etime) - with pytest.raises(TypeError, match=msg): - ts.between_time(stime, etime, axis=0) - - if axis in ["index", 0]: - ts.columns = mask - with pytest.raises(TypeError, match=msg): - ts.between_time(stime, etime, axis=1) - def test_operation_on_NaT(self): # Both NaT and Timestamp are in DataFrame. df = pd.DataFrame({"foo": [pd.NaT, pd.NaT, pd.Timestamp("2012-05-01")]}) @@ -455,95 +158,3 @@ def test_datetime_assignment_with_NaT_and_diff_time_units(self): {0: [1, None], "new": [1e9, None]}, dtype="datetime64[ns]" ) tm.assert_frame_equal(result, expected) - - def test_frame_to_period(self): - K = 5 - - dr = date_range("1/1/2000", "1/1/2001") - pr = period_range("1/1/2000", "1/1/2001") - df = DataFrame(np.random.randn(len(dr), K), index=dr) - df["mix"] = "a" - - pts = df.to_period() - exp = df.copy() - exp.index = pr - tm.assert_frame_equal(pts, exp) - - pts = df.to_period("M") - tm.assert_index_equal(pts.index, exp.index.asfreq("M")) - - df = df.T - pts = df.to_period(axis=1) - exp = df.copy() - exp.columns = pr - tm.assert_frame_equal(pts, exp) - - pts = df.to_period("M", axis=1) - tm.assert_index_equal(pts.columns, exp.columns.asfreq("M")) - - msg = "No axis named 2 for object type " - with pytest.raises(ValueError, match=msg): - df.to_period(axis=2) - - @pytest.mark.parametrize("fn", ["tz_localize", "tz_convert"]) - def test_tz_convert_and_localize(self, fn): - l0 = date_range("20140701", periods=5, freq="D") - l1 = date_range("20140701", periods=5, freq="D") - - int_idx = Index(range(5)) - - if fn == "tz_convert": - l0 = l0.tz_localize("UTC") - l1 = l1.tz_localize("UTC") - - for idx in [l0, l1]: - - l0_expected = getattr(idx, fn)("US/Pacific") - l1_expected = getattr(idx, fn)("US/Pacific") - - df1 = DataFrame(np.ones(5), index=l0) - df1 = getattr(df1, fn)("US/Pacific") - tm.assert_index_equal(df1.index, l0_expected) - - # MultiIndex - # GH7846 - df2 = DataFrame(np.ones(5), MultiIndex.from_arrays([l0, l1])) - - df3 = getattr(df2, fn)("US/Pacific", level=0) - assert not df3.index.levels[0].equals(l0) - tm.assert_index_equal(df3.index.levels[0], l0_expected) - tm.assert_index_equal(df3.index.levels[1], l1) - assert not df3.index.levels[1].equals(l1_expected) - - df3 = getattr(df2, fn)("US/Pacific", level=1) - tm.assert_index_equal(df3.index.levels[0], l0) - assert not df3.index.levels[0].equals(l0_expected) - tm.assert_index_equal(df3.index.levels[1], l1_expected) - assert not df3.index.levels[1].equals(l1) - - df4 = DataFrame(np.ones(5), MultiIndex.from_arrays([int_idx, l0])) - - # TODO: untested - df5 = getattr(df4, fn)("US/Pacific", level=1) # noqa - - tm.assert_index_equal(df3.index.levels[0], l0) - assert not df3.index.levels[0].equals(l0_expected) - tm.assert_index_equal(df3.index.levels[1], l1_expected) - assert not df3.index.levels[1].equals(l1) - - # Bad Inputs - - # Not DatetimeIndex / PeriodIndex - with pytest.raises(TypeError, match="DatetimeIndex"): - df = DataFrame(index=int_idx) - df = getattr(df, fn)("US/Pacific") - - # Not DatetimeIndex / PeriodIndex - with pytest.raises(TypeError, match="DatetimeIndex"): - df = DataFrame(np.ones(5), MultiIndex.from_arrays([int_idx, l0])) - df = getattr(df, fn)("US/Pacific", level=0) - - # Invalid level - with pytest.raises(ValueError, match="not valid"): - df = DataFrame(index=l0) - df = getattr(df, fn)("US/Pacific", level=1) diff --git a/pandas/tests/frame/test_timezones.py b/pandas/tests/frame/test_timezones.py index b60f2052a988f..dfd4fb1855383 100644 --- a/pandas/tests/frame/test_timezones.py +++ b/pandas/tests/frame/test_timezones.py @@ -1,8 +1,6 @@ """ Tests for DataFrame timezone-related methods """ -from datetime import datetime - import numpy as np import pytest import pytz @@ -53,40 +51,6 @@ def test_frame_values_with_tz(self): result = df.values tm.assert_numpy_array_equal(result, expected) - def test_frame_from_records_utc(self): - rec = {"datum": 1.5, "begin_time": datetime(2006, 4, 27, tzinfo=pytz.utc)} - - # it works - DataFrame.from_records([rec], index="begin_time") - - def test_frame_tz_localize(self): - rng = date_range("1/1/2011", periods=100, freq="H") - - df = DataFrame({"a": 1}, index=rng) - result = df.tz_localize("utc") - expected = DataFrame({"a": 1}, rng.tz_localize("UTC")) - assert result.index.tz.zone == "UTC" - tm.assert_frame_equal(result, expected) - - df = df.T - result = df.tz_localize("utc", axis=1) - assert result.columns.tz.zone == "UTC" - tm.assert_frame_equal(result, expected.T) - - def test_frame_tz_convert(self): - rng = date_range("1/1/2011", periods=200, freq="D", tz="US/Eastern") - - df = DataFrame({"a": 1}, index=rng) - result = df.tz_convert("Europe/Berlin") - expected = DataFrame({"a": 1}, rng.tz_convert("Europe/Berlin")) - assert result.index.tz.zone == "Europe/Berlin" - tm.assert_frame_equal(result, expected) - - df = df.T - result = df.tz_convert("Europe/Berlin", axis=1) - assert result.columns.tz.zone == "Europe/Berlin" - tm.assert_frame_equal(result, expected.T) - def test_frame_join_tzaware(self): test1 = DataFrame( np.zeros((6, 3)), @@ -108,17 +72,6 @@ def test_frame_join_tzaware(self): tm.assert_index_equal(result.index, ex_index) assert result.index.tz.zone == "US/Central" - def test_frame_add_tz_mismatch_converts_to_utc(self): - rng = date_range("1/1/2011", periods=10, freq="H", tz="US/Eastern") - df = DataFrame(np.random.randn(len(rng)), index=rng, columns=["a"]) - - df_moscow = df.tz_convert("Europe/Moscow") - result = df + df_moscow - assert result.index.tz is pytz.utc - - result = df_moscow + df - assert result.index.tz is pytz.utc - def test_frame_align_aware(self): idx1 = date_range("2001", periods=5, freq="H", tz="US/Eastern") idx2 = date_range("2001", periods=5, freq="2H", tz="US/Eastern") diff --git a/pandas/tests/frame/test_to_csv.py b/pandas/tests/frame/test_to_csv.py index aeff92971b42a..86c9a98377f3f 100644 --- a/pandas/tests/frame/test_to_csv.py +++ b/pandas/tests/frame/test_to_csv.py @@ -687,7 +687,7 @@ def _make_frame(names=None): df.to_csv(path) for i in [6, 7]: - msg = "len of {i}, but only 5 lines in file".format(i=i) + msg = f"len of {i}, but only 5 lines in file" with pytest.raises(ParserError, match=msg): read_csv(path, header=list(range(i)), index_col=0) @@ -744,7 +744,7 @@ def test_to_csv_withcommas(self): def test_to_csv_mixed(self): def create_cols(name): - return ["{name}{i:03d}".format(name=name, i=i) for i in range(5)] + return [f"{name}{i:03d}" for i in range(5)] df_float = DataFrame( np.random.randn(100, 5), dtype="float64", columns=create_cols("float") diff --git a/pandas/tests/generic/test_frame.py b/pandas/tests/generic/test_frame.py index 7fe22e77c5bf3..dca65152e82db 100644 --- a/pandas/tests/generic/test_frame.py +++ b/pandas/tests/generic/test_frame.py @@ -32,19 +32,20 @@ def test_rename_mi(self): ) df.rename(str.lower) - def test_set_axis_name(self): + @pytest.mark.parametrize("func", ["_set_axis_name", "rename_axis"]) + def test_set_axis_name(self, func): df = pd.DataFrame([[1, 2], [3, 4]]) - funcs = ["_set_axis_name", "rename_axis"] - for func in funcs: - result = methodcaller(func, "foo")(df) - assert df.index.name is None - assert result.index.name == "foo" - result = methodcaller(func, "cols", axis=1)(df) - assert df.columns.name is None - assert result.columns.name == "cols" + result = methodcaller(func, "foo")(df) + assert df.index.name is None + assert result.index.name == "foo" - def test_set_axis_name_mi(self): + result = methodcaller(func, "cols", axis=1)(df) + assert df.columns.name is None + assert result.columns.name == "cols" + + @pytest.mark.parametrize("func", ["_set_axis_name", "rename_axis"]) + def test_set_axis_name_mi(self, func): df = DataFrame( np.empty((3, 3)), index=MultiIndex.from_tuples([("A", x) for x in list("aBc")]), @@ -52,15 +53,14 @@ def test_set_axis_name_mi(self): ) level_names = ["L1", "L2"] - funcs = ["_set_axis_name", "rename_axis"] - for func in funcs: - result = methodcaller(func, level_names)(df) - assert result.index.names == level_names - assert result.columns.names == [None, None] - result = methodcaller(func, level_names, axis=1)(df) - assert result.columns.names == ["L1", "L2"] - assert result.index.names == [None, None] + result = methodcaller(func, level_names)(df) + assert result.index.names == level_names + assert result.columns.names == [None, None] + + result = methodcaller(func, level_names, axis=1)(df) + assert result.columns.names == ["L1", "L2"] + assert result.index.names == [None, None] def test_nonzero_single_element(self): @@ -160,7 +160,7 @@ def finalize(self, other, method=None, **kwargs): # reset DataFrame._metadata = _metadata - DataFrame.__finalize__ = _finalize + DataFrame.__finalize__ = _finalize # FIXME: use monkeypatch def test_set_attribute(self): # Test for consistent setattr behavior when an attribute and a column @@ -174,29 +174,78 @@ def test_set_attribute(self): assert df.y == 5 tm.assert_series_equal(df["y"], Series([2, 4, 6], name="y")) + def test_deepcopy_empty(self): + # This test covers empty frame copying with non-empty column sets + # as reported in issue GH15370 + empty_frame = DataFrame(data=[], index=[], columns=["A"]) + empty_frame_copy = deepcopy(empty_frame) + + self._compare(empty_frame_copy, empty_frame) + + +# formerly in Generic but only test DataFrame +class TestDataFrame2: + @pytest.mark.parametrize("value", [1, "True", [1, 2, 3], 5.0]) + def test_validate_bool_args(self, value): + df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + + with pytest.raises(ValueError): + super(DataFrame, df).rename_axis( + mapper={"a": "x", "b": "y"}, axis=1, inplace=value + ) + + with pytest.raises(ValueError): + super(DataFrame, df).drop("a", axis=1, inplace=value) + + with pytest.raises(ValueError): + super(DataFrame, df)._consolidate(inplace=value) + + with pytest.raises(ValueError): + super(DataFrame, df).fillna(value=0, inplace=value) + + with pytest.raises(ValueError): + super(DataFrame, df).replace(to_replace=1, value=7, inplace=value) + + with pytest.raises(ValueError): + super(DataFrame, df).interpolate(inplace=value) + + with pytest.raises(ValueError): + super(DataFrame, df)._where(cond=df.a > 2, inplace=value) + + with pytest.raises(ValueError): + super(DataFrame, df).mask(cond=df.a > 2, inplace=value) + + def test_unexpected_keyword(self): + # GH8597 + df = DataFrame(np.random.randn(5, 2), columns=["jim", "joe"]) + ca = pd.Categorical([0, 0, 2, 2, 3, np.nan]) + ts = df["joe"].copy() + ts[2] = np.nan + + with pytest.raises(TypeError, match="unexpected keyword"): + df.drop("joe", axis=1, in_place=True) + + with pytest.raises(TypeError, match="unexpected keyword"): + df.reindex([1, 0], inplace=True) + + with pytest.raises(TypeError, match="unexpected keyword"): + ca.fillna(0, inplace=True) + + with pytest.raises(TypeError, match="unexpected keyword"): + ts.fillna(0, in_place=True) + + +class TestToXArray: @pytest.mark.skipif( not _XARRAY_INSTALLED or _XARRAY_INSTALLED and LooseVersion(xarray.__version__) < LooseVersion("0.10.0"), reason="xarray >= 0.10.0 required", ) - @pytest.mark.parametrize( - "index", - [ - "FloatIndex", - "IntIndex", - "StringIndex", - "UnicodeIndex", - "DateIndex", - "PeriodIndex", - "CategoricalIndex", - "TimedeltaIndex", - ], - ) + @pytest.mark.parametrize("index", tm.all_index_generator(3)) def test_to_xarray_index_types(self, index): from xarray import Dataset - index = getattr(tm, f"make{index}") df = DataFrame( { "a": list("abc"), @@ -210,7 +259,7 @@ def test_to_xarray_index_types(self, index): } ) - df.index = index(3) + df.index = index df.index.name = "foo" df.columns.name = "bar" result = df.to_xarray() @@ -272,11 +321,3 @@ def test_to_xarray(self): expected["f"] = expected["f"].astype(object) expected.columns.name = None tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_deepcopy_empty(self): - # This test covers empty frame copying with non-empty column sets - # as reported in issue GH15370 - empty_frame = DataFrame(data=[], index=[], columns=["A"]) - empty_frame_copy = deepcopy(empty_frame) - - self._compare(empty_frame_copy, empty_frame) diff --git a/pandas/tests/generic/test_generic.py b/pandas/tests/generic/test_generic.py index 7645c6b4cf709..1b6cb8447c76d 100644 --- a/pandas/tests/generic/test_generic.py +++ b/pandas/tests/generic/test_generic.py @@ -23,10 +23,11 @@ def _axes(self): return self._typ._AXIS_ORDERS def _construct(self, shape, value=None, dtype=None, **kwargs): - """ construct an object for the given shape - if value is specified use that if its a scalar - if value is an array, repeat it as needed """ - + """ + construct an object for the given shape + if value is specified use that if its a scalar + if value is an array, repeat it as needed + """ if isinstance(shape, int): shape = tuple([shape] * self._ndim) if value is not None: @@ -103,23 +104,6 @@ def test_get_numeric_data(self): # _get_numeric_data is includes _get_bool_data, so can't test for # non-inclusion - def test_get_default(self): - - # GH 7725 - d0 = "a", "b", "c", "d" - d1 = np.arange(4, dtype="int64") - others = "e", 10 - - for data, index in ((d0, d1), (d1, d0)): - s = Series(data, index=index) - for i, d in zip(index, data): - assert s.get(i) == d - assert s.get(i, d) == d - assert s.get(i, "z") == d - for other in others: - assert s.get(other, "z") == "z" - assert s.get(other, other) == other - def test_nonzero(self): # GH 4633 @@ -203,8 +187,10 @@ def test_constructor_compound_dtypes(self): def f(dtype): return self._construct(shape=3, value=1, dtype=dtype) - msg = "compound dtypes are not implemented" - f"in the {self._typ.__name__} constructor" + msg = ( + "compound dtypes are not implemented " + f"in the {self._typ.__name__} constructor" + ) with pytest.raises(NotImplementedError, match=msg): f([("A", "datetime64[h]"), ("B", "str"), ("C", "int32")]) @@ -273,39 +259,31 @@ def test_metadata_propagation(self): self.check_metadata(v1 & v2) self.check_metadata(v1 | v2) - def test_head_tail(self): + @pytest.mark.parametrize("index", tm.all_index_generator(10)) + def test_head_tail(self, index): # GH5370 o = self._construct(shape=10) - # check all index types - for index in [ - tm.makeFloatIndex, - tm.makeIntIndex, - tm.makeStringIndex, - tm.makeUnicodeIndex, - tm.makeDateIndex, - tm.makePeriodIndex, - ]: - axis = o._get_axis_name(0) - setattr(o, axis, index(len(getattr(o, axis)))) + axis = o._get_axis_name(0) + setattr(o, axis, index) - o.head() + o.head() - self._compare(o.head(), o.iloc[:5]) - self._compare(o.tail(), o.iloc[-5:]) + self._compare(o.head(), o.iloc[:5]) + self._compare(o.tail(), o.iloc[-5:]) - # 0-len - self._compare(o.head(0), o.iloc[0:0]) - self._compare(o.tail(0), o.iloc[0:0]) + # 0-len + self._compare(o.head(0), o.iloc[0:0]) + self._compare(o.tail(0), o.iloc[0:0]) - # bounded - self._compare(o.head(len(o) + 1), o) - self._compare(o.tail(len(o) + 1), o) + # bounded + self._compare(o.head(len(o) + 1), o) + self._compare(o.tail(len(o) + 1), o) - # neg index - self._compare(o.head(-3), o.head(7)) - self._compare(o.tail(-3), o.tail(7)) + # neg index + self._compare(o.head(-3), o.head(7)) + self._compare(o.tail(-3), o.tail(7)) def test_sample(self): # Fixes issue: 2419 @@ -469,24 +447,6 @@ def test_split_compat(self): assert len(np.array_split(o, 5)) == 5 assert len(np.array_split(o, 2)) == 2 - def test_unexpected_keyword(self): # GH8597 - df = DataFrame(np.random.randn(5, 2), columns=["jim", "joe"]) - ca = pd.Categorical([0, 0, 2, 2, 3, np.nan]) - ts = df["joe"].copy() - ts[2] = np.nan - - with pytest.raises(TypeError, match="unexpected keyword"): - df.drop("joe", axis=1, in_place=True) - - with pytest.raises(TypeError, match="unexpected keyword"): - df.reindex([1, 0], inplace=True) - - with pytest.raises(TypeError, match="unexpected keyword"): - ca.fillna(0, inplace=True) - - with pytest.raises(TypeError, match="unexpected keyword"): - ts.fillna(0, in_place=True) - # See gh-12301 def test_stat_unexpected_keyword(self): obj = self._construct(5) @@ -502,16 +462,16 @@ def test_stat_unexpected_keyword(self): with pytest.raises(TypeError, match=errmsg): obj.any(epic=starwars) # logical_function - def test_api_compat(self): + @pytest.mark.parametrize("func", ["sum", "cumsum", "any", "var"]) + def test_api_compat(self, func): # GH 12021 # compat for __name__, __qualname__ obj = self._construct(5) - for func in ["sum", "cumsum", "any", "var"]: - f = getattr(obj, func) - assert f.__name__ == func - assert f.__qualname__.endswith(func) + f = getattr(obj, func) + assert f.__name__ == func + assert f.__qualname__.endswith(func) def test_stat_non_defaults_args(self): obj = self._construct(5) @@ -544,50 +504,17 @@ def test_truncate_out_of_bounds(self): self._compare(big.truncate(before=0, after=3e6), big) self._compare(big.truncate(before=-1, after=2e6), big) - def test_validate_bool_args(self): - df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) - invalid_values = [1, "True", [1, 2, 3], 5.0] - - for value in invalid_values: - with pytest.raises(ValueError): - super(DataFrame, df).rename_axis( - mapper={"a": "x", "b": "y"}, axis=1, inplace=value - ) - - with pytest.raises(ValueError): - super(DataFrame, df).drop("a", axis=1, inplace=value) - - with pytest.raises(ValueError): - super(DataFrame, df)._consolidate(inplace=value) - - with pytest.raises(ValueError): - super(DataFrame, df).fillna(value=0, inplace=value) - - with pytest.raises(ValueError): - super(DataFrame, df).replace(to_replace=1, value=7, inplace=value) - - with pytest.raises(ValueError): - super(DataFrame, df).interpolate(inplace=value) - - with pytest.raises(ValueError): - super(DataFrame, df)._where(cond=df.a > 2, inplace=value) - - with pytest.raises(ValueError): - super(DataFrame, df).mask(cond=df.a > 2, inplace=value) - - def test_copy_and_deepcopy(self): + @pytest.mark.parametrize( + "func", + [copy, deepcopy, lambda x: x.copy(deep=False), lambda x: x.copy(deep=True)], + ) + @pytest.mark.parametrize("shape", [0, 1, 2]) + def test_copy_and_deepcopy(self, shape, func): # GH 15444 - for shape in [0, 1, 2]: - obj = self._construct(shape) - for func in [ - copy, - deepcopy, - lambda x: x.copy(deep=False), - lambda x: x.copy(deep=True), - ]: - obj_copy = func(obj) - assert obj_copy is not obj - self._compare(obj_copy, obj) + obj = self._construct(shape) + obj_copy = func(obj) + assert obj_copy is not obj + self._compare(obj_copy, obj) @pytest.mark.parametrize( "periods,fill_method,limit,exp", diff --git a/pandas/tests/generic/test_series.py b/pandas/tests/generic/test_series.py index 8ad8355f2d530..f119eb422a276 100644 --- a/pandas/tests/generic/test_series.py +++ b/pandas/tests/generic/test_series.py @@ -24,13 +24,6 @@ class TestSeries(Generic): _typ = Series _comparator = lambda self, x, y: tm.assert_series_equal(x, y) - def setup_method(self): - self.ts = tm.makeTimeSeries() # Was at top level in test_series - self.ts.name = "ts" - - self.series = tm.makeStringSeries() - self.series.name = "series" - def test_rename_mi(self): s = Series( [11, 21, 31], @@ -38,29 +31,29 @@ def test_rename_mi(self): ) s.rename(str.lower) - def test_set_axis_name(self): + @pytest.mark.parametrize("func", ["rename_axis", "_set_axis_name"]) + def test_set_axis_name(self, func): s = Series([1, 2, 3], index=["a", "b", "c"]) - funcs = ["rename_axis", "_set_axis_name"] name = "foo" - for func in funcs: - result = methodcaller(func, name)(s) - assert s.index.name is None - assert result.index.name == name - def test_set_axis_name_mi(self): + result = methodcaller(func, name)(s) + assert s.index.name is None + assert result.index.name == name + + @pytest.mark.parametrize("func", ["rename_axis", "_set_axis_name"]) + def test_set_axis_name_mi(self, func): s = Series( [11, 21, 31], index=MultiIndex.from_tuples( [("A", x) for x in ["a", "B", "c"]], names=["l1", "l2"] ), ) - funcs = ["rename_axis", "_set_axis_name"] - for func in funcs: - result = methodcaller(func, ["L1", "L2"])(s) - assert s.index.name is None - assert s.index.names == ["l1", "l2"] - assert result.index.name is None - assert result.index.names, ["L1", "L2"] + + result = methodcaller(func, ["L1", "L2"])(s) + assert s.index.name is None + assert s.index.names == ["l1", "l2"] + assert result.index.name is None + assert result.index.names, ["L1", "L2"] def test_set_axis_name_raises(self): s = pd.Series([1]) @@ -181,32 +174,60 @@ def finalize(self, other, method=None, **kwargs): # reset Series._metadata = _metadata - Series.__finalize__ = _finalize + Series.__finalize__ = _finalize # FIXME: use monkeypatch + + @pytest.mark.parametrize( + "s", + [ + Series([np.arange(5)]), + pd.date_range("1/1/2011", periods=24, freq="H"), + pd.Series(range(5), index=pd.date_range("2017", periods=5)), + ], + ) + @pytest.mark.parametrize("shift_size", [0, 1, 2]) + def test_shift_always_copy(self, s, shift_size): + # GH22397 + assert s.shift(shift_size) is not s + + @pytest.mark.parametrize("move_by_freq", [pd.Timedelta("1D"), pd.Timedelta("1M")]) + def test_datetime_shift_always_copy(self, move_by_freq): + # GH22397 + s = pd.Series(range(5), index=pd.date_range("2017", periods=5)) + assert s.shift(freq=move_by_freq) is not s + + +class TestSeries2: + # moved from Generic + def test_get_default(self): + + # GH#7725 + d0 = ["a", "b", "c", "d"] + d1 = np.arange(4, dtype="int64") + others = ["e", 10] + for data, index in ((d0, d1), (d1, d0)): + s = Series(data, index=index) + for i, d in zip(index, data): + assert s.get(i) == d + assert s.get(i, d) == d + assert s.get(i, "z") == d + for other in others: + assert s.get(other, "z") == "z" + assert s.get(other, other) == other + + +class TestToXArray: @pytest.mark.skipif( not _XARRAY_INSTALLED or _XARRAY_INSTALLED and LooseVersion(xarray.__version__) < LooseVersion("0.10.0"), reason="xarray >= 0.10.0 required", ) - @pytest.mark.parametrize( - "index", - [ - "FloatIndex", - "IntIndex", - "StringIndex", - "UnicodeIndex", - "DateIndex", - "PeriodIndex", - "TimedeltaIndex", - "CategoricalIndex", - ], - ) + @pytest.mark.parametrize("index", tm.all_index_generator(6)) def test_to_xarray_index_types(self, index): from xarray import DataArray - index = getattr(tm, f"make{index}") - s = Series(range(6), index=index(6)) + s = Series(range(6), index=index) s.index.name = "foo" result = s.to_xarray() repr(result) @@ -242,22 +263,3 @@ def test_to_xarray(self): tm.assert_almost_equal(list(result.coords.keys()), ["one", "two"]) assert isinstance(result, DataArray) tm.assert_series_equal(result.to_series(), s) - - @pytest.mark.parametrize( - "s", - [ - Series([np.arange(5)]), - pd.date_range("1/1/2011", periods=24, freq="H"), - pd.Series(range(5), index=pd.date_range("2017", periods=5)), - ], - ) - @pytest.mark.parametrize("shift_size", [0, 1, 2]) - def test_shift_always_copy(self, s, shift_size): - # GH22397 - assert s.shift(shift_size) is not s - - @pytest.mark.parametrize("move_by_freq", [pd.Timedelta("1D"), pd.Timedelta("1M")]) - def test_datetime_shift_always_copy(self, move_by_freq): - # GH22397 - s = pd.Series(range(5), index=pd.date_range("2017", periods=5)) - assert s.shift(freq=move_by_freq) is not s diff --git a/pandas/tests/groupby/aggregate/test_aggregate.py b/pandas/tests/groupby/aggregate/test_aggregate.py index ff99081521ffb..48f8de7e51ae4 100644 --- a/pandas/tests/groupby/aggregate/test_aggregate.py +++ b/pandas/tests/groupby/aggregate/test_aggregate.py @@ -13,6 +13,18 @@ from pandas.core.groupby.grouper import Grouping +def test_groupby_agg_no_extra_calls(): + # GH#31760 + df = pd.DataFrame({"key": ["a", "b", "c", "c"], "value": [1, 2, 3, 4]}) + gb = df.groupby("key")["value"] + + def dummy_func(x): + assert len(x) != 0 + return x.sum() + + gb.agg(dummy_func) + + def test_agg_regression1(tsframe): grouped = tsframe.groupby([lambda x: x.year, lambda x: x.month]) result = grouped.agg(np.mean) diff --git a/pandas/tests/groupby/conftest.py b/pandas/tests/groupby/conftest.py index ebac36c5f8c78..1214734358c80 100644 --- a/pandas/tests/groupby/conftest.py +++ b/pandas/tests/groupby/conftest.py @@ -107,7 +107,8 @@ def three_group(): @pytest.fixture(params=sorted(reduction_kernels)) def reduction_func(request): - """yields the string names of all groupby reduction functions, one at a time. + """ + yields the string names of all groupby reduction functions, one at a time. """ return request.param diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index 41ec70468aaeb..18ad5d90b3f60 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -108,8 +108,9 @@ def f(g): splitter = grouper._get_splitter(g._selected_obj, axis=g.axis) group_keys = grouper._get_group_keys() + sdata = splitter._get_sorted_data() - values, mutated = splitter.fast_apply(f, group_keys) + values, mutated = splitter.fast_apply(f, sdata, group_keys) assert not mutated diff --git a/pandas/tests/groupby/test_bin_groupby.py b/pandas/tests/groupby/test_bin_groupby.py index ad71f73e80e64..ff74d374e5e3f 100644 --- a/pandas/tests/groupby/test_bin_groupby.py +++ b/pandas/tests/groupby/test_bin_groupby.py @@ -11,7 +11,7 @@ def test_series_grouper(): obj = Series(np.random.randn(10)) - dummy = obj[:0] + dummy = obj.iloc[:0] labels = np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1, 1], dtype=np.int64) @@ -28,7 +28,7 @@ def test_series_grouper(): def test_series_grouper_requires_nonempty_raises(): # GH#29500 obj = Series(np.random.randn(10)) - dummy = obj[:0] + dummy = obj.iloc[:0] labels = np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1, 1], dtype=np.int64) with pytest.raises(ValueError, match="SeriesGrouper requires non-empty `series`"): diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py index 1c2de8c8c223f..9b07269811d8e 100644 --- a/pandas/tests/groupby/test_categorical.py +++ b/pandas/tests/groupby/test_categorical.py @@ -20,7 +20,8 @@ def cartesian_product_for_groupers(result, args, names): """ Reindex to a cartesian production for the groupers, - preserving the nature (Categorical) of each grouper """ + preserving the nature (Categorical) of each grouper + """ def f(a): if isinstance(a, (CategoricalIndex, Categorical)): diff --git a/pandas/tests/groupby/test_function.py b/pandas/tests/groupby/test_function.py index 73e36cb5e6c84..c402ca194648f 100644 --- a/pandas/tests/groupby/test_function.py +++ b/pandas/tests/groupby/test_function.py @@ -17,6 +17,7 @@ NaT, Series, Timestamp, + _is_numpy_dev, date_range, isna, ) @@ -25,6 +26,26 @@ from pandas.util import _test_decorators as td +@pytest.fixture( + params=[np.int32, np.int64, np.float32, np.float64], + ids=["np.int32", "np.int64", "np.float32", "np.float64"], +) +def numpy_dtypes_for_minmax(request): + """ + Fixture of numpy dtypes with min and max values used for testing + cummin and cummax + """ + dtype = request.param + min_val = ( + np.iinfo(dtype).min if np.dtype(dtype).kind == "i" else np.finfo(dtype).min + ) + max_val = ( + np.iinfo(dtype).max if np.dtype(dtype).kind == "i" else np.finfo(dtype).max + ) + + return (dtype, min_val, max_val) + + @pytest.mark.parametrize("agg_func", ["any", "all"]) @pytest.mark.parametrize("skipna", [True, False]) @pytest.mark.parametrize( @@ -173,11 +194,10 @@ def test_arg_passthru(): ) for attr in ["mean", "median"]: - f = getattr(df.groupby("group"), attr) - result = f() + result = getattr(df.groupby("group"), attr)() tm.assert_index_equal(result.columns, expected_columns_numeric) - result = f(numeric_only=False) + result = getattr(df.groupby("group"), attr)(numeric_only=False) tm.assert_frame_equal(result.reindex_like(expected), expected) # TODO: min, max *should* handle @@ -194,11 +214,10 @@ def test_arg_passthru(): ] ) for attr in ["min", "max"]: - f = getattr(df.groupby("group"), attr) - result = f() + result = getattr(df.groupby("group"), attr)() tm.assert_index_equal(result.columns, expected_columns) - result = f(numeric_only=False) + result = getattr(df.groupby("group"), attr)(numeric_only=False) tm.assert_index_equal(result.columns, expected_columns) expected_columns = Index( @@ -214,29 +233,26 @@ def test_arg_passthru(): ] ) for attr in ["first", "last"]: - f = getattr(df.groupby("group"), attr) - result = f() + result = getattr(df.groupby("group"), attr)() tm.assert_index_equal(result.columns, expected_columns) - result = f(numeric_only=False) + result = getattr(df.groupby("group"), attr)(numeric_only=False) tm.assert_index_equal(result.columns, expected_columns) expected_columns = Index(["int", "float", "string", "category_int", "timedelta"]) - for attr in ["sum"]: - f = getattr(df.groupby("group"), attr) - result = f() - tm.assert_index_equal(result.columns, expected_columns_numeric) - result = f(numeric_only=False) - tm.assert_index_equal(result.columns, expected_columns) + result = df.groupby("group").sum() + tm.assert_index_equal(result.columns, expected_columns_numeric) + + result = df.groupby("group").sum(numeric_only=False) + tm.assert_index_equal(result.columns, expected_columns) expected_columns = Index(["int", "float", "category_int"]) for attr in ["prod", "cumprod"]: - f = getattr(df.groupby("group"), attr) - result = f() + result = getattr(df.groupby("group"), attr)() tm.assert_index_equal(result.columns, expected_columns_numeric) - result = f(numeric_only=False) + result = getattr(df.groupby("group"), attr)(numeric_only=False) tm.assert_index_equal(result.columns, expected_columns) # like min, max, but don't include strings @@ -244,22 +260,20 @@ def test_arg_passthru(): ["int", "float", "category_int", "datetime", "datetimetz", "timedelta"] ) for attr in ["cummin", "cummax"]: - f = getattr(df.groupby("group"), attr) - result = f() + result = getattr(df.groupby("group"), attr)() # GH 15561: numeric_only=False set by default like min/max tm.assert_index_equal(result.columns, expected_columns) - result = f(numeric_only=False) + result = getattr(df.groupby("group"), attr)(numeric_only=False) tm.assert_index_equal(result.columns, expected_columns) expected_columns = Index(["int", "float", "category_int", "timedelta"]) - for attr in ["cumsum"]: - f = getattr(df.groupby("group"), attr) - result = f() - tm.assert_index_equal(result.columns, expected_columns_numeric) - result = f(numeric_only=False) - tm.assert_index_equal(result.columns, expected_columns) + result = getattr(df.groupby("group"), "cumsum")() + tm.assert_index_equal(result.columns, expected_columns_numeric) + + result = getattr(df.groupby("group"), "cumsum")(numeric_only=False) + tm.assert_index_equal(result.columns, expected_columns) def test_non_cython_api(): @@ -685,59 +699,36 @@ def test_numpy_compat(func): getattr(g, func)(foo=1) -def test_cummin_cummax(): +@pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, +) +def test_cummin(numpy_dtypes_for_minmax): + dtype = numpy_dtypes_for_minmax[0] + min_val = numpy_dtypes_for_minmax[1] + # GH 15048 - num_types = [np.int32, np.int64, np.float32, np.float64] - num_mins = [ - np.iinfo(np.int32).min, - np.iinfo(np.int64).min, - np.finfo(np.float32).min, - np.finfo(np.float64).min, - ] - num_max = [ - np.iinfo(np.int32).max, - np.iinfo(np.int64).max, - np.finfo(np.float32).max, - np.finfo(np.float64).max, - ] base_df = pd.DataFrame( {"A": [1, 1, 1, 1, 2, 2, 2, 2], "B": [3, 4, 3, 2, 2, 3, 2, 1]} ) expected_mins = [3, 3, 3, 2, 2, 2, 2, 1] - expected_maxs = [3, 4, 4, 4, 2, 3, 3, 3] - - for dtype, min_val, max_val in zip(num_types, num_mins, num_max): - df = base_df.astype(dtype) - # cummin - expected = pd.DataFrame({"B": expected_mins}).astype(dtype) - result = df.groupby("A").cummin() - tm.assert_frame_equal(result, expected) - result = df.groupby("A").B.apply(lambda x: x.cummin()).to_frame() - tm.assert_frame_equal(result, expected) - - # Test cummin w/ min value for dtype - df.loc[[2, 6], "B"] = min_val - expected.loc[[2, 3, 6, 7], "B"] = min_val - result = df.groupby("A").cummin() - tm.assert_frame_equal(result, expected) - expected = df.groupby("A").B.apply(lambda x: x.cummin()).to_frame() - tm.assert_frame_equal(result, expected) + df = base_df.astype(dtype) - # cummax - expected = pd.DataFrame({"B": expected_maxs}).astype(dtype) - result = df.groupby("A").cummax() - tm.assert_frame_equal(result, expected) - result = df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() - tm.assert_frame_equal(result, expected) + expected = pd.DataFrame({"B": expected_mins}).astype(dtype) + result = df.groupby("A").cummin() + tm.assert_frame_equal(result, expected) + result = df.groupby("A").B.apply(lambda x: x.cummin()).to_frame() + tm.assert_frame_equal(result, expected) - # Test cummax w/ max value for dtype - df.loc[[2, 6], "B"] = max_val - expected.loc[[2, 3, 6, 7], "B"] = max_val - result = df.groupby("A").cummax() - tm.assert_frame_equal(result, expected) - expected = df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() - tm.assert_frame_equal(result, expected) + # Test w/ min value for dtype + df.loc[[2, 6], "B"] = min_val + expected.loc[[2, 3, 6, 7], "B"] = min_val + result = df.groupby("A").cummin() + tm.assert_frame_equal(result, expected) + expected = df.groupby("A").B.apply(lambda x: x.cummin()).to_frame() + tm.assert_frame_equal(result, expected) # Test nan in some values base_df.loc[[0, 2, 4, 6], "B"] = np.nan @@ -747,30 +738,80 @@ def test_cummin_cummax(): expected = base_df.groupby("A").B.apply(lambda x: x.cummin()).to_frame() tm.assert_frame_equal(result, expected) - expected = pd.DataFrame({"B": [np.nan, 4, np.nan, 4, np.nan, 3, np.nan, 3]}) - result = base_df.groupby("A").cummax() - tm.assert_frame_equal(result, expected) - expected = base_df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() - tm.assert_frame_equal(result, expected) + # GH 15561 + df = pd.DataFrame(dict(a=[1], b=pd.to_datetime(["2001"]))) + expected = pd.Series(pd.to_datetime("2001"), index=[0], name="b") + + result = df.groupby("a")["b"].cummin() + tm.assert_series_equal(expected, result) + + # GH 15635 + df = pd.DataFrame(dict(a=[1, 2, 1], b=[1, 2, 2])) + result = df.groupby("a").b.cummin() + expected = pd.Series([1, 2, 1], name="b") + tm.assert_series_equal(result, expected) + + +@pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, +) +def test_cummin_all_nan_column(): + base_df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 2, 2], "B": [np.nan] * 8}) - # Test nan in entire column - base_df["B"] = np.nan expected = pd.DataFrame({"B": [np.nan] * 8}) result = base_df.groupby("A").cummin() tm.assert_frame_equal(expected, result) result = base_df.groupby("A").B.apply(lambda x: x.cummin()).to_frame() tm.assert_frame_equal(expected, result) + + +@pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, +) +def test_cummax(numpy_dtypes_for_minmax): + dtype = numpy_dtypes_for_minmax[0] + max_val = numpy_dtypes_for_minmax[2] + + # GH 15048 + base_df = pd.DataFrame( + {"A": [1, 1, 1, 1, 2, 2, 2, 2], "B": [3, 4, 3, 2, 2, 3, 2, 1]} + ) + expected_maxs = [3, 4, 4, 4, 2, 3, 3, 3] + + df = base_df.astype(dtype) + + expected = pd.DataFrame({"B": expected_maxs}).astype(dtype) + result = df.groupby("A").cummax() + tm.assert_frame_equal(result, expected) + result = df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() + tm.assert_frame_equal(result, expected) + + # Test w/ max value for dtype + df.loc[[2, 6], "B"] = max_val + expected.loc[[2, 3, 6, 7], "B"] = max_val + result = df.groupby("A").cummax() + tm.assert_frame_equal(result, expected) + expected = df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() + tm.assert_frame_equal(result, expected) + + # Test nan in some values + base_df.loc[[0, 2, 4, 6], "B"] = np.nan + expected = pd.DataFrame({"B": [np.nan, 4, np.nan, 4, np.nan, 3, np.nan, 3]}) result = base_df.groupby("A").cummax() - tm.assert_frame_equal(expected, result) - result = base_df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() - tm.assert_frame_equal(expected, result) + tm.assert_frame_equal(result, expected) + expected = base_df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() + tm.assert_frame_equal(result, expected) # GH 15561 df = pd.DataFrame(dict(a=[1], b=pd.to_datetime(["2001"]))) expected = pd.Series(pd.to_datetime("2001"), index=[0], name="b") - for method in ["cummax", "cummin"]: - result = getattr(df.groupby("a")["b"], method)() - tm.assert_series_equal(expected, result) + + result = df.groupby("a")["b"].cummax() + tm.assert_series_equal(expected, result) # GH 15635 df = pd.DataFrame(dict(a=[1, 2, 1], b=[2, 1, 1])) @@ -778,10 +819,20 @@ def test_cummin_cummax(): expected = pd.Series([2, 1, 2], name="b") tm.assert_series_equal(result, expected) - df = pd.DataFrame(dict(a=[1, 2, 1], b=[1, 2, 2])) - result = df.groupby("a").b.cummin() - expected = pd.Series([1, 2, 1], name="b") - tm.assert_series_equal(result, expected) + +@pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, +) +def test_cummax_all_nan_column(): + base_df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 2, 2], "B": [np.nan] * 8}) + + expected = pd.DataFrame({"B": [np.nan] * 8}) + result = base_df.groupby("A").cummax() + tm.assert_frame_equal(expected, result) + result = base_df.groupby("A").B.apply(lambda x: x.cummax()).to_frame() + tm.assert_frame_equal(expected, result) @pytest.mark.parametrize( @@ -966,6 +1017,7 @@ def test_frame_describe_unstacked_format(): @pytest.mark.parametrize("dropna", [False, True]) def test_series_groupby_nunique(n, m, sort, dropna): def check_nunique(df, keys, as_index=True): + original_df = df.copy() gr = df.groupby(keys, as_index=as_index, sort=sort) left = gr["julie"].nunique(dropna=dropna) @@ -975,6 +1027,7 @@ def check_nunique(df, keys, as_index=True): right = right.reset_index(drop=True) tm.assert_series_equal(left, right, check_names=False) + tm.assert_frame_equal(df, original_df) days = date_range("2015-08-23", periods=10) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index b7d7124a3a5e5..5662d41e19885 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -1496,7 +1496,7 @@ def test_groupby_reindex_inside_function(): def agg_before(hour, func, fix=False): """ - Run an aggregate func on the subset of data. + Run an aggregate func on the subset of data. """ def _func(data): diff --git a/pandas/tests/groupby/test_nth.py b/pandas/tests/groupby/test_nth.py index 0f850f2e94581..b1476f1059d84 100644 --- a/pandas/tests/groupby/test_nth.py +++ b/pandas/tests/groupby/test_nth.py @@ -54,6 +54,46 @@ def test_first_last_nth(df): tm.assert_frame_equal(result, expected) +@pytest.mark.parametrize("method", ["first", "last"]) +def test_first_last_with_na_object(method, nulls_fixture): + # https://github.com/pandas-dev/pandas/issues/32123 + groups = pd.DataFrame({"a": [1, 1, 2, 2], "b": [1, 2, 3, nulls_fixture]}).groupby( + "a" + ) + result = getattr(groups, method)() + + if method == "first": + values = [1, 3] + else: + values = [2, 3] + + values = np.array(values, dtype=result["b"].dtype) + idx = pd.Index([1, 2], name="a") + expected = pd.DataFrame({"b": values}, index=idx) + + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index", [0, -1]) +def test_nth_with_na_object(index, nulls_fixture): + # https://github.com/pandas-dev/pandas/issues/32123 + groups = pd.DataFrame({"a": [1, 1, 2, 2], "b": [1, 2, 3, nulls_fixture]}).groupby( + "a" + ) + result = groups.nth(index) + + if index == 0: + values = [1, 3] + else: + values = [2, nulls_fixture] + + values = np.array(values, dtype=result["b"].dtype) + idx = pd.Index([1, 2], name="a") + expected = pd.DataFrame({"b": values}, index=idx) + + tm.assert_frame_equal(result, expected) + + def test_first_last_nth_dtypes(df_mixed_floats): df = df_mixed_floats.copy() diff --git a/pandas/tests/groupby/test_transform.py b/pandas/tests/groupby/test_transform.py index 8967ef06f50fb..740103eec185a 100644 --- a/pandas/tests/groupby/test_transform.py +++ b/pandas/tests/groupby/test_transform.py @@ -15,6 +15,7 @@ MultiIndex, Series, Timestamp, + _is_numpy_dev, concat, date_range, ) @@ -329,6 +330,8 @@ def test_transform_transformation_func(transformation_func): if transformation_func in ["pad", "backfill", "tshift", "corrwith", "cumcount"]: # These transformation functions are not yet covered in this test pytest.xfail("See GH 31269 and GH 31270") + elif _is_numpy_dev and transformation_func in ["cummin"]: + pytest.xfail("https://github.com/pandas-dev/pandas/issues/31992") elif transformation_func == "fillna": test_op = lambda x: x.transform("fillna", value=0) mock_op = lambda x: x.fillna(value=0) @@ -520,7 +523,6 @@ def _check_cython_group_transform_cumulative(pd_op, np_op, dtype): dtype : type The specified dtype of the data. """ - is_datetimelike = False data = np.array([[1], [2], [3], [4]], dtype=dtype) diff --git a/pandas/tests/indexes/base_class/test_reshape.py b/pandas/tests/indexes/base_class/test_reshape.py new file mode 100644 index 0000000000000..61826f2403a4b --- /dev/null +++ b/pandas/tests/indexes/base_class/test_reshape.py @@ -0,0 +1,61 @@ +""" +Tests for ndarray-like method on the base Index class +""" +import pytest + +import pandas as pd +from pandas import Index +import pandas._testing as tm + + +class TestReshape: + def test_repeat(self): + repeats = 2 + index = pd.Index([1, 2, 3]) + expected = pd.Index([1, 1, 2, 2, 3, 3]) + + result = index.repeat(repeats) + tm.assert_index_equal(result, expected) + + def test_insert(self): + + # GH 7256 + # validate neg/pos inserts + result = Index(["b", "c", "d"]) + + # test 0th element + tm.assert_index_equal(Index(["a", "b", "c", "d"]), result.insert(0, "a")) + + # test Nth element that follows Python list behavior + tm.assert_index_equal(Index(["b", "c", "e", "d"]), result.insert(-1, "e")) + + # test loc +/- neq (0, -1) + tm.assert_index_equal(result.insert(1, "z"), result.insert(-2, "z")) + + # test empty + null_index = Index([]) + tm.assert_index_equal(Index(["a"]), null_index.insert(0, "a")) + + @pytest.mark.parametrize( + "pos,expected", + [ + (0, Index(["b", "c", "d"], name="index")), + (-1, Index(["a", "b", "c"], name="index")), + ], + ) + def test_delete(self, pos, expected): + index = Index(["a", "b", "c", "d"], name="index") + result = index.delete(pos) + tm.assert_index_equal(result, expected) + assert result.name == expected.name + + def test_append_multiple(self): + index = Index(["a", "b", "c", "d", "e", "f"]) + + foos = [index[:2], index[2:4], index[4:]] + result = foos[0].append(foos[1:]) + tm.assert_index_equal(result, index) + + # empty + result = index.append([]) + tm.assert_index_equal(result, index) diff --git a/pandas/tests/indexes/base_class/test_setops.py b/pandas/tests/indexes/base_class/test_setops.py index e7d5e21d0ba47..ec3ef8050967c 100644 --- a/pandas/tests/indexes/base_class/test_setops.py +++ b/pandas/tests/indexes/base_class/test_setops.py @@ -1,12 +1,49 @@ import numpy as np import pytest +import pandas as pd from pandas import Index, Series import pandas._testing as tm from pandas.core.algorithms import safe_sort class TestIndexSetOps: + @pytest.mark.parametrize( + "method", ["union", "intersection", "difference", "symmetric_difference"] + ) + def test_setops_disallow_true(self, method): + idx1 = pd.Index(["a", "b"]) + idx2 = pd.Index(["b", "c"]) + + with pytest.raises(ValueError, match="The 'sort' keyword only takes"): + getattr(idx1, method)(idx2, sort=True) + + def test_setops_preserve_object_dtype(self): + idx = pd.Index([1, 2, 3], dtype=object) + result = idx.intersection(idx[1:]) + expected = idx[1:] + tm.assert_index_equal(result, expected) + + # if other is not monotonic increasing, intersection goes through + # a different route + result = idx.intersection(idx[1:][::-1]) + tm.assert_index_equal(result, expected) + + result = idx._union(idx[1:], sort=None) + expected = idx + tm.assert_index_equal(result, expected) + + result = idx.union(idx[1:], sort=None) + tm.assert_index_equal(result, expected) + + # if other is not monotonic increasing, _union goes through + # a different route + result = idx._union(idx[1:][::-1], sort=None) + tm.assert_index_equal(result, expected) + + result = idx.union(idx[1:][::-1], sort=None) + tm.assert_index_equal(result, expected) + def test_union_base(self): index = Index([0, "a", 1, "b", 2, "c"]) first = index[3:] @@ -28,6 +65,32 @@ def test_union_different_type_base(self, klass): assert tm.equalContents(result, index) + def test_union_sort_other_incomparable(self): + # https://github.com/pandas-dev/pandas/issues/24959 + idx = pd.Index([1, pd.Timestamp("2000")]) + # default (sort=None) + with tm.assert_produces_warning(RuntimeWarning): + result = idx.union(idx[:1]) + + tm.assert_index_equal(result, idx) + + # sort=None + with tm.assert_produces_warning(RuntimeWarning): + result = idx.union(idx[:1], sort=None) + tm.assert_index_equal(result, idx) + + # sort=False + result = idx.union(idx[:1], sort=False) + tm.assert_index_equal(result, idx) + + @pytest.mark.xfail(reason="Not implemented") + def test_union_sort_other_incomparable_true(self): + # TODO decide on True behaviour + # sort=True + idx = pd.Index([1, pd.Timestamp("2000")]) + with pytest.raises(TypeError, match=".*"): + idx.union(idx[:1], sort=True) + @pytest.mark.parametrize("sort", [None, False]) def test_intersection_base(self, sort): # (same results for py2 and py3 but sortedness not tested elsewhere) @@ -50,6 +113,16 @@ def test_intersection_different_type_base(self, klass, sort): result = first.intersection(klass(second.values), sort=sort) assert tm.equalContents(result, second) + def test_intersect_nosort(self): + result = pd.Index(["c", "b", "a"]).intersection(["b", "a"]) + expected = pd.Index(["b", "a"]) + tm.assert_index_equal(result, expected) + + def test_intersection_equal_sort(self): + idx = pd.Index(["c", "a", "b"]) + tm.assert_index_equal(idx.intersection(idx, sort=False), idx) + tm.assert_index_equal(idx.intersection(idx, sort=None), idx) + @pytest.mark.parametrize("sort", [None, False]) def test_difference_base(self, sort): # (same results for py2 and py3 but sortedness not tested elsewhere) diff --git a/pandas/tests/indexes/categorical/test_constructors.py b/pandas/tests/indexes/categorical/test_constructors.py index 1df0874e2f947..ee3f85da22781 100644 --- a/pandas/tests/indexes/categorical/test_constructors.py +++ b/pandas/tests/indexes/categorical/test_constructors.py @@ -136,12 +136,3 @@ def test_construction_with_categorical_dtype(self): with pytest.raises(ValueError, match=msg): Index(data, ordered=ordered, dtype=dtype) - - def test_create_categorical(self): - # GH#17513 The public CI constructor doesn't hit this code path with - # instances of CategoricalIndex, but we still want to test the code - ci = CategoricalIndex(["a", "b", "c"]) - # First ci is self, second ci is data. - result = CategoricalIndex._create_categorical(ci, ci) - expected = Categorical(["a", "b", "c"]) - tm.assert_categorical_equal(result, expected) diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py index da27057a783ab..dca317a9eb03f 100644 --- a/pandas/tests/indexes/common.py +++ b/pandas/tests/indexes/common.py @@ -98,7 +98,7 @@ def test_shift(self): # GH8083 test the base class for shift idx = self.create_index() - msg = "Not supported for type {}".format(type(idx).__name__) + msg = f"Not supported for type {type(idx).__name__}" with pytest.raises(NotImplementedError, match=msg): idx.shift(1) with pytest.raises(NotImplementedError, match=msg): @@ -514,12 +514,12 @@ def test_union_base(self, indices): @pytest.mark.parametrize("sort", [None, False]) def test_difference_base(self, sort, indices): - if isinstance(indices, CategoricalIndex): - return - first = indices[2:] second = indices[:4] - answer = indices[4:] + if isinstance(indices, CategoricalIndex) or indices.is_boolean(): + answer = [] + else: + answer = indices[4:] result = first.difference(second, sort) assert tm.equalContents(result, answer) @@ -605,7 +605,8 @@ def test_equals(self, indices): assert not indices.equals(np.array(indices)) # Cannot pass in non-int64 dtype to RangeIndex - if not isinstance(indices, RangeIndex): + if not isinstance(indices, (RangeIndex, CategoricalIndex)): + # TODO: CategoricalIndex can be re-allowed following GH#32167 same_values = Index(indices, dtype=object) assert indices.equals(same_values) assert same_values.equals(indices) @@ -808,7 +809,7 @@ def test_map_dictlike(self, mapper): index = self.create_index() if isinstance(index, (pd.CategoricalIndex, pd.IntervalIndex)): - pytest.skip("skipping tests for {}".format(type(index))) + pytest.skip(f"skipping tests for {type(index)}") identity = mapper(index.values, index) diff --git a/pandas/tests/indexes/conftest.py b/pandas/tests/indexes/conftest.py deleted file mode 100644 index 57174f206b70d..0000000000000 --- a/pandas/tests/indexes/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest - -import pandas._testing as tm -from pandas.core.indexes.api import Index, MultiIndex - -indices_dict = { - "unicode": tm.makeUnicodeIndex(100), - "string": tm.makeStringIndex(100), - "datetime": tm.makeDateIndex(100), - "datetime-tz": tm.makeDateIndex(100, tz="US/Pacific"), - "period": tm.makePeriodIndex(100), - "timedelta": tm.makeTimedeltaIndex(100), - "int": tm.makeIntIndex(100), - "uint": tm.makeUIntIndex(100), - "range": tm.makeRangeIndex(100), - "float": tm.makeFloatIndex(100), - "bool": tm.makeBoolIndex(2), - "categorical": tm.makeCategoricalIndex(100), - "interval": tm.makeIntervalIndex(100), - "empty": Index([]), - "tuples": MultiIndex.from_tuples(zip(["foo", "bar", "baz"], [1, 2, 3])), - "repeats": Index([0, 0, 1, 1, 2, 2]), -} - - -@pytest.fixture(params=indices_dict.keys()) -def indices(request): - # copy to avoid mutation, e.g. setting .name - return indices_dict[request.param].copy() diff --git a/pandas/tests/indexes/datetimelike.py b/pandas/tests/indexes/datetimelike.py index 3c72d34d84b28..ba10976a67e9a 100644 --- a/pandas/tests/indexes/datetimelike.py +++ b/pandas/tests/indexes/datetimelike.py @@ -36,7 +36,7 @@ def test_str(self): # test the string repr idx = self.create_index() idx.name = "foo" - assert not "length={}".format(len(idx)) in str(idx) + assert not (f"length={len(idx)}" in str(idx)) assert "'foo'" in str(idx) assert type(idx).__name__ in str(idx) @@ -44,7 +44,7 @@ def test_str(self): if idx.tz is not None: assert idx.tz in str(idx) if hasattr(idx, "freq"): - assert "freq='{idx.freqstr}'".format(idx=idx) in str(idx) + assert f"freq='{idx.freqstr}'" in str(idx) def test_view(self): i = self.create_index() diff --git a/pandas/tests/indexes/datetimes/test_astype.py b/pandas/tests/indexes/datetimes/test_astype.py index 6139726dc34e4..916f722247a14 100644 --- a/pandas/tests/indexes/datetimes/test_astype.py +++ b/pandas/tests/indexes/datetimes/test_astype.py @@ -1,7 +1,6 @@ from datetime import datetime import dateutil -from dateutil.tz import tzlocal import numpy as np import pytest import pytz @@ -12,7 +11,7 @@ Index, Int64Index, NaT, - Period, + PeriodIndex, Series, Timestamp, date_range, @@ -278,81 +277,19 @@ def test_integer_index_astype_datetime(self, tz, dtype): expected = pd.DatetimeIndex(["2018-01-01"], tz=tz) tm.assert_index_equal(result, expected) + def test_dti_astype_period(self): + idx = DatetimeIndex([NaT, "2011-01-01", "2011-02-01"], name="idx") -class TestToPeriod: - def setup_method(self, method): - data = [ - Timestamp("2007-01-01 10:11:12.123456Z"), - Timestamp("2007-01-01 10:11:13.789123Z"), - ] - self.index = DatetimeIndex(data) - - def test_to_period_millisecond(self): - index = self.index - - with tm.assert_produces_warning(UserWarning): - # warning that timezone info will be lost - period = index.to_period(freq="L") - assert 2 == len(period) - assert period[0] == Period("2007-01-01 10:11:12.123Z", "L") - assert period[1] == Period("2007-01-01 10:11:13.789Z", "L") - - def test_to_period_microsecond(self): - index = self.index + res = idx.astype("period[M]") + exp = PeriodIndex(["NaT", "2011-01", "2011-02"], freq="M", name="idx") + tm.assert_index_equal(res, exp) - with tm.assert_produces_warning(UserWarning): - # warning that timezone info will be lost - period = index.to_period(freq="U") - assert 2 == len(period) - assert period[0] == Period("2007-01-01 10:11:12.123456Z", "U") - assert period[1] == Period("2007-01-01 10:11:13.789123Z", "U") - - @pytest.mark.parametrize( - "tz", - ["US/Eastern", pytz.utc, tzlocal(), "dateutil/US/Eastern", dateutil.tz.tzutc()], - ) - def test_to_period_tz(self, tz): - ts = date_range("1/1/2000", "2/1/2000", tz=tz) - - with tm.assert_produces_warning(UserWarning): - # GH#21333 warning that timezone info will be lost - result = ts.to_period()[0] - expected = ts[0].to_period() - - assert result == expected - - expected = date_range("1/1/2000", "2/1/2000").to_period() - - with tm.assert_produces_warning(UserWarning): - # GH#21333 warning that timezone info will be lost - result = ts.to_period() - - tm.assert_index_equal(result, expected) + res = idx.astype("period[3M]") + exp = PeriodIndex(["NaT", "2011-01", "2011-02"], freq="3M", name="idx") + tm.assert_index_equal(res, exp) - @pytest.mark.parametrize("tz", ["Etc/GMT-1", "Etc/GMT+1"]) - def test_to_period_tz_utc_offset_consistency(self, tz): - # GH 22905 - ts = pd.date_range("1/1/2000", "2/1/2000", tz="Etc/GMT-1") - with tm.assert_produces_warning(UserWarning): - result = ts.to_period()[0] - expected = ts[0].to_period() - assert result == expected - - def test_to_period_nofreq(self): - idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-04"]) - with pytest.raises(ValueError): - idx.to_period() - - idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-03"], freq="infer") - assert idx.freqstr == "D" - expected = pd.PeriodIndex(["2000-01-01", "2000-01-02", "2000-01-03"], freq="D") - tm.assert_index_equal(idx.to_period(), expected) - - # GH 7606 - idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-03"]) - assert idx.freqstr is None - tm.assert_index_equal(idx.to_period(), expected) +class TestAstype: @pytest.mark.parametrize("tz", [None, "US/Central"]) def test_astype_category(self, tz): obj = pd.date_range("2000", periods=2, tz=tz) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 1d1d371fcec1e..b293c008d6683 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from functools import partial from operator import attrgetter @@ -959,3 +959,95 @@ def test_pass_datetimeindex_to_index(self): expected = Index(rng.to_pydatetime(), dtype=object) tm.assert_numpy_array_equal(idx.values, expected.values) + + +def test_timestamp_constructor_invalid_fold_raise(): + # Test for #25057 + # Valid fold values are only [None, 0, 1] + msg = "Valid values for the fold argument are None, 0, or 1." + with pytest.raises(ValueError, match=msg): + Timestamp(123, fold=2) + + +def test_timestamp_constructor_pytz_fold_raise(): + # Test for #25057 + # pytz doesn't support fold. Check that we raise + # if fold is passed with pytz + msg = "pytz timezones do not support fold. Please use dateutil timezones." + tz = pytz.timezone("Europe/London") + with pytest.raises(ValueError, match=msg): + Timestamp(datetime(2019, 10, 27, 0, 30, 0, 0), tz=tz, fold=0) + + +@pytest.mark.parametrize("fold", [0, 1]) +@pytest.mark.parametrize( + "ts_input", + [ + 1572136200000000000, + 1572136200000000000.0, + np.datetime64(1572136200000000000, "ns"), + "2019-10-27 01:30:00+01:00", + datetime(2019, 10, 27, 0, 30, 0, 0, tzinfo=timezone.utc), + ], +) +def test_timestamp_constructor_fold_conflict(ts_input, fold): + # Test for #25057 + # Check that we raise on fold conflict + msg = ( + "Cannot pass fold with possibly unambiguous input: int, float, " + "numpy.datetime64, str, or timezone-aware datetime-like. " + "Pass naive datetime-like or build Timestamp from components." + ) + with pytest.raises(ValueError, match=msg): + Timestamp(ts_input=ts_input, fold=fold) + + +@pytest.mark.parametrize("tz", ["dateutil/Europe/London", None]) +@pytest.mark.parametrize("fold", [0, 1]) +def test_timestamp_constructor_retain_fold(tz, fold): + # Test for #25057 + # Check that we retain fold + ts = pd.Timestamp(year=2019, month=10, day=27, hour=1, minute=30, tz=tz, fold=fold) + result = ts.fold + expected = fold + assert result == expected + + +@pytest.mark.parametrize("tz", ["dateutil/Europe/London"]) +@pytest.mark.parametrize( + "ts_input,fold_out", + [ + (1572136200000000000, 0), + (1572139800000000000, 1), + ("2019-10-27 01:30:00+01:00", 0), + ("2019-10-27 01:30:00+00:00", 1), + (datetime(2019, 10, 27, 1, 30, 0, 0, fold=0), 0), + (datetime(2019, 10, 27, 1, 30, 0, 0, fold=1), 1), + ], +) +def test_timestamp_constructor_infer_fold_from_value(tz, ts_input, fold_out): + # Test for #25057 + # Check that we infer fold correctly based on timestamps since utc + # or strings + ts = pd.Timestamp(ts_input, tz=tz) + result = ts.fold + expected = fold_out + assert result == expected + + +@pytest.mark.parametrize("tz", ["dateutil/Europe/London"]) +@pytest.mark.parametrize( + "ts_input,fold,value_out", + [ + (datetime(2019, 10, 27, 1, 30, 0, 0), 0, 1572136200000000000), + (datetime(2019, 10, 27, 1, 30, 0, 0), 1, 1572139800000000000), + ], +) +def test_timestamp_constructor_adjust_value_for_fold(tz, ts_input, fold, value_out): + # Test for #25057 + # Check that we adjust value for fold correctly + # based on timestamps since utc + ts = pd.Timestamp(ts_input, tz=tz, fold=fold) + result = ts.value + expected = value_out + assert result == expected diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 4d0beecbbf5d3..d33351fe94a8c 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -759,17 +759,6 @@ def test_constructor(self): with pytest.raises(TypeError, match=msg): bdate_range(START, END, periods=10, freq=None) - def test_naive_aware_conflicts(self): - naive = bdate_range(START, END, freq=BDay(), tz=None) - aware = bdate_range(START, END, freq=BDay(), tz="Asia/Hong_Kong") - - msg = "tz-naive.*tz-aware" - with pytest.raises(TypeError, match=msg): - naive.join(aware) - - with pytest.raises(TypeError, match=msg): - aware.join(naive) - def test_misc(self): end = datetime(2009, 5, 13) dr = bdate_range(end=end, periods=20) diff --git a/pandas/tests/indexes/datetimes/test_datetime.py b/pandas/tests/indexes/datetimes/test_datetime.py index ca18d6fbea11a..6217f225d496e 100644 --- a/pandas/tests/indexes/datetimes/test_datetime.py +++ b/pandas/tests/indexes/datetimes/test_datetime.py @@ -5,7 +5,7 @@ import pytest import pandas as pd -from pandas import DataFrame, DatetimeIndex, Index, Timestamp, date_range, offsets +from pandas import DataFrame, DatetimeIndex, Index, NaT, Timestamp, date_range, offsets import pandas._testing as tm randn = np.random.randn @@ -20,6 +20,24 @@ def test_roundtrip_pickle_with_tz(self): unpickled = tm.round_trip_pickle(index) tm.assert_index_equal(index, unpickled) + def test_pickle(self): + + # GH#4606 + p = tm.round_trip_pickle(NaT) + assert p is NaT + + idx = pd.to_datetime(["2013-01-01", NaT, "2014-01-06"]) + idx_p = tm.round_trip_pickle(idx) + assert idx_p[0] == idx[0] + assert idx_p[1] is NaT + assert idx_p[2] == idx[2] + + # GH#11002 + # don't infer freq + idx = date_range("1750-1-1", "2050-1-1", freq="7D") + idx_p = tm.round_trip_pickle(idx) + tm.assert_index_equal(idx, idx_p) + def test_reindex_preserves_tz_if_target_is_empty_list_or_array(self): # GH7774 index = date_range("20130101", periods=3, tz="US/Eastern") @@ -100,16 +118,13 @@ def test_stringified_slice_with_tz(self): df = DataFrame(np.arange(10), index=idx) df["2013-01-14 23:44:34.437768-05:00":] # no exception here - def test_append_join_nondatetimeindex(self): + def test_append_nondatetimeindex(self): rng = date_range("1/1/2000", periods=10) idx = Index(["a", "b", "c", "d"]) result = rng.append(idx) assert isinstance(result[0], Timestamp) - # it works - rng.join(idx, how="outer") - def test_map(self): rng = date_range("1/1/2000", periods=10) @@ -246,25 +261,6 @@ def test_isin(self): index.isin([index[2], 5]), np.array([False, False, True, False]) ) - def test_does_not_convert_mixed_integer(self): - df = tm.makeCustomDataframe( - 10, - 10, - data_gen_f=lambda *args, **kwargs: randn(), - r_idx_type="i", - c_idx_type="dt", - ) - cols = df.columns.join(df.index, how="outer") - joined = cols.join(df.columns) - assert cols.dtype == np.dtype("O") - assert cols.dtype == joined.dtype - tm.assert_numpy_array_equal(cols.values, joined.values) - - def test_join_self(self, join_type): - index = date_range("1/1/2000", periods=10) - joined = index.join(index, how=join_type) - assert index is joined - def assert_index_parameters(self, index): assert index.freq == "40960N" assert index.inferred_freq == "40960N" @@ -282,20 +278,6 @@ def test_ns_index(self): new_index = pd.date_range(start=index[0], end=index[-1], freq=index.freq) self.assert_index_parameters(new_index) - def test_join_with_period_index(self, join_type): - df = tm.makeCustomDataframe( - 10, - 10, - data_gen_f=lambda *args: np.random.randint(2), - c_idx_type="p", - r_idx_type="dt", - ) - s = df.iloc[:5, 0] - - expected = df.columns.astype("O").join(s.index, how=join_type) - result = df.columns.join(s.index, how=join_type) - tm.assert_index_equal(expected, result) - def test_factorize(self): idx1 = DatetimeIndex( ["2014-01", "2014-01", "2014-02", "2014-02", "2014-03", "2014-03"] diff --git a/pandas/tests/indexes/datetimes/test_indexing.py b/pandas/tests/indexes/datetimes/test_indexing.py index c358e72538788..ceab670fb5041 100644 --- a/pandas/tests/indexes/datetimes/test_indexing.py +++ b/pandas/tests/indexes/datetimes/test_indexing.py @@ -423,6 +423,17 @@ def test_get_loc(self): with pytest.raises(NotImplementedError): idx.get_loc(time(12, 30), method="pad") + def test_get_loc_tz_aware(self): + # https://github.com/pandas-dev/pandas/issues/32140 + dti = pd.date_range( + pd.Timestamp("2019-12-12 00:00:00", tz="US/Eastern"), + pd.Timestamp("2019-12-13 00:00:00", tz="US/Eastern"), + freq="5s", + ) + key = pd.Timestamp("2019-12-12 10:19:25", tz="US/Eastern") + result = dti.get_loc(key, method="nearest") + assert result == 7433 + def test_get_loc_nat(self): # GH#20464 index = DatetimeIndex(["1/3/2000", "NaT"]) diff --git a/pandas/tests/indexes/datetimes/test_join.py b/pandas/tests/indexes/datetimes/test_join.py new file mode 100644 index 0000000000000..f2f88fd7dc90c --- /dev/null +++ b/pandas/tests/indexes/datetimes/test_join.py @@ -0,0 +1,144 @@ +from datetime import datetime + +import numpy as np +import pytest + +from pandas import DatetimeIndex, Index, Timestamp, date_range, to_datetime +import pandas._testing as tm + +from pandas.tseries.offsets import BDay, BMonthEnd + + +class TestJoin: + def test_does_not_convert_mixed_integer(self): + df = tm.makeCustomDataframe( + 10, + 10, + data_gen_f=lambda *args, **kwargs: np.random.randn(), + r_idx_type="i", + c_idx_type="dt", + ) + cols = df.columns.join(df.index, how="outer") + joined = cols.join(df.columns) + assert cols.dtype == np.dtype("O") + assert cols.dtype == joined.dtype + tm.assert_numpy_array_equal(cols.values, joined.values) + + def test_join_self(self, join_type): + index = date_range("1/1/2000", periods=10) + joined = index.join(index, how=join_type) + assert index is joined + + def test_join_with_period_index(self, join_type): + df = tm.makeCustomDataframe( + 10, + 10, + data_gen_f=lambda *args: np.random.randint(2), + c_idx_type="p", + r_idx_type="dt", + ) + s = df.iloc[:5, 0] + + expected = df.columns.astype("O").join(s.index, how=join_type) + result = df.columns.join(s.index, how=join_type) + tm.assert_index_equal(expected, result) + + def test_join_object_index(self): + rng = date_range("1/1/2000", periods=10) + idx = Index(["a", "b", "c", "d"]) + + result = rng.join(idx, how="outer") + assert isinstance(result[0], Timestamp) + + def test_join_utc_convert(self, join_type): + rng = date_range("1/1/2011", periods=100, freq="H", tz="utc") + + left = rng.tz_convert("US/Eastern") + right = rng.tz_convert("Europe/Berlin") + + result = left.join(left[:-5], how=join_type) + assert isinstance(result, DatetimeIndex) + assert result.tz == left.tz + + result = left.join(right[:-5], how=join_type) + assert isinstance(result, DatetimeIndex) + assert result.tz.zone == "UTC" + + @pytest.mark.parametrize("sort", [None, False]) + def test_datetimeindex_union_join_empty(self, sort): + dti = date_range(start="1/1/2001", end="2/1/2001", freq="D") + empty = Index([]) + + result = dti.union(empty, sort=sort) + expected = dti.astype("O") + tm.assert_index_equal(result, expected) + + result = dti.join(empty) + assert isinstance(result, DatetimeIndex) + tm.assert_index_equal(result, dti) + + def test_join_nonunique(self): + idx1 = to_datetime(["2012-11-06 16:00:11.477563", "2012-11-06 16:00:11.477563"]) + idx2 = to_datetime(["2012-11-06 15:11:09.006507", "2012-11-06 15:11:09.006507"]) + rs = idx1.join(idx2, how="outer") + assert rs.is_monotonic + + @pytest.mark.parametrize("freq", ["B", "C"]) + def test_outer_join(self, freq): + # should just behave as union + start, end = datetime(2009, 1, 1), datetime(2010, 1, 1) + rng = date_range(start=start, end=end, freq=freq) + + # overlapping + left = rng[:10] + right = rng[5:10] + + the_join = left.join(right, how="outer") + assert isinstance(the_join, DatetimeIndex) + + # non-overlapping, gap in middle + left = rng[:5] + right = rng[10:] + + the_join = left.join(right, how="outer") + assert isinstance(the_join, DatetimeIndex) + assert the_join.freq is None + + # non-overlapping, no gap + left = rng[:5] + right = rng[5:10] + + the_join = left.join(right, how="outer") + assert isinstance(the_join, DatetimeIndex) + + # overlapping, but different offset + other = date_range(start, end, freq=BMonthEnd()) + + the_join = rng.join(other, how="outer") + assert isinstance(the_join, DatetimeIndex) + assert the_join.freq is None + + def test_naive_aware_conflicts(self): + start, end = datetime(2009, 1, 1), datetime(2010, 1, 1) + naive = date_range(start, end, freq=BDay(), tz=None) + aware = date_range(start, end, freq=BDay(), tz="Asia/Hong_Kong") + + msg = "tz-naive.*tz-aware" + with pytest.raises(TypeError, match=msg): + naive.join(aware) + + with pytest.raises(TypeError, match=msg): + aware.join(naive) + + @pytest.mark.parametrize("tz", [None, "US/Pacific"]) + def test_join_preserves_freq(self, tz): + # GH#32157 + dti = date_range("2016-01-01", periods=10, tz=tz) + result = dti[:5].join(dti[5:], how="outer") + assert result.freq == dti.freq + tm.assert_index_equal(result, dti) + + result = dti[:5].join(dti[6:], how="outer") + assert result.freq is None + expected = dti.delete(5) + tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/datetimes/test_scalar_compat.py b/pandas/tests/indexes/datetimes/test_scalar_compat.py index 84eee2419f0b8..21ee8649172da 100644 --- a/pandas/tests/indexes/datetimes/test_scalar_compat.py +++ b/pandas/tests/indexes/datetimes/test_scalar_compat.py @@ -248,21 +248,21 @@ def test_round_int64(self, start, index_freq, periods, round_freq): 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 (mod == 0).all(), f"floor not a {round_freq} multiple" 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 (mod == 0).all(), f"ceil not a {round_freq} multiple" 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 (mod == 0).all(), f"round not a {round_freq} multiple" assert (diff <= unit // 2).all(), "round error" if unit % 2 == 0: assert ( diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index 78188c54b1d85..ba069f5245de4 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -14,7 +14,6 @@ Series, bdate_range, date_range, - to_datetime, ) import pandas._testing as tm @@ -348,24 +347,40 @@ def test_datetimeindex_diff(self, sort): dti2 = date_range(freq="Q-JAN", start=datetime(1997, 12, 31), periods=98) assert len(dti1.difference(dti2, sort)) == 2 - @pytest.mark.parametrize("sort", [None, False]) - def test_datetimeindex_union_join_empty(self, sort): - dti = date_range(start="1/1/2001", end="2/1/2001", freq="D") - empty = Index([]) + @pytest.mark.parametrize("tz", [None, "Asia/Tokyo", "US/Eastern"]) + def test_setops_preserve_freq(self, tz): + rng = date_range("1/1/2000", "1/1/2002", name="idx", tz=tz) - result = dti.union(empty, sort=sort) - expected = dti.astype("O") - tm.assert_index_equal(result, expected) + result = rng[:50].union(rng[50:100]) + assert result.name == rng.name + assert result.freq == rng.freq + assert result.tz == rng.tz - result = dti.join(empty) - assert isinstance(result, DatetimeIndex) - tm.assert_index_equal(result, dti) + result = rng[:50].union(rng[30:100]) + assert result.name == rng.name + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].union(rng[60:100]) + assert result.name == rng.name + assert result.freq is None + assert result.tz == rng.tz + + result = rng[:50].intersection(rng[25:75]) + assert result.name == rng.name + assert result.freqstr == "D" + assert result.tz == rng.tz - def test_join_nonunique(self): - idx1 = to_datetime(["2012-11-06 16:00:11.477563", "2012-11-06 16:00:11.477563"]) - idx2 = to_datetime(["2012-11-06 15:11:09.006507", "2012-11-06 15:11:09.006507"]) - rs = idx1.join(idx2, how="outer") - assert rs.is_monotonic + nofreq = DatetimeIndex(list(rng[25:75]), name="other") + result = rng[:50].union(nofreq) + assert result.name is None + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].intersection(nofreq) + assert result.name is None + assert result.freq == rng.freq + assert result.tz == rng.tz class TestBusinessDatetimeIndex: @@ -408,38 +423,6 @@ def test_union(self, sort): the_union = self.rng.union(rng, sort=sort) assert isinstance(the_union, DatetimeIndex) - def test_outer_join(self): - # should just behave as union - - # overlapping - left = self.rng[:10] - right = self.rng[5:10] - - the_join = left.join(right, how="outer") - assert isinstance(the_join, DatetimeIndex) - - # non-overlapping, gap in middle - left = self.rng[:5] - right = self.rng[10:] - - the_join = left.join(right, how="outer") - assert isinstance(the_join, DatetimeIndex) - assert the_join.freq is None - - # non-overlapping, no gap - left = self.rng[:5] - right = self.rng[5:10] - - the_join = left.join(right, how="outer") - assert isinstance(the_join, DatetimeIndex) - - # overlapping, but different offset - rng = date_range(START, END, freq=BMonthEnd()) - - the_join = self.rng.join(rng, how="outer") - assert isinstance(the_join, DatetimeIndex) - assert the_join.freq is None - @pytest.mark.parametrize("sort", [None, False]) def test_union_not_cacheable(self, sort): rng = date_range("1/1/2000", periods=50, freq=Minute()) @@ -556,38 +539,6 @@ def test_union(self, sort): the_union = self.rng.union(rng, sort=sort) assert isinstance(the_union, DatetimeIndex) - def test_outer_join(self): - # should just behave as union - - # overlapping - left = self.rng[:10] - right = self.rng[5:10] - - the_join = left.join(right, how="outer") - assert isinstance(the_join, DatetimeIndex) - - # non-overlapping, gap in middle - left = self.rng[:5] - right = self.rng[10:] - - the_join = left.join(right, how="outer") - assert isinstance(the_join, DatetimeIndex) - assert the_join.freq is None - - # non-overlapping, no gap - left = self.rng[:5] - right = self.rng[5:10] - - the_join = left.join(right, how="outer") - assert isinstance(the_join, DatetimeIndex) - - # overlapping, but different offset - rng = date_range(START, END, freq=BMonthEnd()) - - the_join = self.rng.join(rng, how="outer") - assert isinstance(the_join, DatetimeIndex) - assert the_join.freq is None - def test_intersection_bug(self): # GH #771 a = bdate_range("11/30/2011", "12/31/2011", freq="C") diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index cd8e8c3542cce..9c1e8cb0f563f 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -791,7 +791,6 @@ def test_dti_tz_constructors(self, tzstr): """ Test different DatetimeIndex constructions with timezone Follow-up of GH#4229 """ - arr = ["11/10/2005 08:00:00", "11/10/2005 09:00:00"] idx1 = to_datetime(arr).tz_localize(tzstr) @@ -805,20 +804,6 @@ def test_dti_tz_constructors(self, tzstr): # ------------------------------------------------------------- # Unsorted - def test_join_utc_convert(self, join_type): - rng = date_range("1/1/2011", periods=100, freq="H", tz="utc") - - left = rng.tz_convert("US/Eastern") - right = rng.tz_convert("Europe/Berlin") - - result = left.join(left[:-5], how=join_type) - assert isinstance(result, DatetimeIndex) - assert result.tz == left.tz - - result = left.join(right[:-5], how=join_type) - assert isinstance(result, DatetimeIndex) - assert result.tz.zone == "UTC" - @pytest.mark.parametrize( "dtype", [None, "datetime64[ns, CET]", "datetime64[ns, EST]", "datetime64[ns, UTC]"], diff --git a/pandas/tests/indexes/datetimes/test_to_period.py b/pandas/tests/indexes/datetimes/test_to_period.py new file mode 100644 index 0000000000000..ddbb43787abb4 --- /dev/null +++ b/pandas/tests/indexes/datetimes/test_to_period.py @@ -0,0 +1,161 @@ +import dateutil.tz +from dateutil.tz import tzlocal +import pytest +import pytz + +from pandas._libs.tslibs.ccalendar import MONTHS +from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG + +from pandas import ( + DatetimeIndex, + Period, + PeriodIndex, + Timestamp, + date_range, + period_range, +) +import pandas._testing as tm + + +class TestToPeriod: + def test_dti_to_period(self): + dti = date_range(start="1/1/2005", end="12/1/2005", freq="M") + pi1 = dti.to_period() + pi2 = dti.to_period(freq="D") + pi3 = dti.to_period(freq="3D") + + assert pi1[0] == Period("Jan 2005", freq="M") + assert pi2[0] == Period("1/31/2005", freq="D") + assert pi3[0] == Period("1/31/2005", freq="3D") + + assert pi1[-1] == Period("Nov 2005", freq="M") + assert pi2[-1] == Period("11/30/2005", freq="D") + assert pi3[-1], Period("11/30/2005", freq="3D") + + tm.assert_index_equal(pi1, period_range("1/1/2005", "11/1/2005", freq="M")) + tm.assert_index_equal( + pi2, period_range("1/1/2005", "11/1/2005", freq="M").asfreq("D") + ) + tm.assert_index_equal( + pi3, period_range("1/1/2005", "11/1/2005", freq="M").asfreq("3D") + ) + + @pytest.mark.parametrize("month", MONTHS) + def test_to_period_quarterly(self, month): + # make sure we can make the round trip + freq = f"Q-{month}" + rng = period_range("1989Q3", "1991Q3", freq=freq) + stamps = rng.to_timestamp() + result = stamps.to_period(freq) + tm.assert_index_equal(rng, result) + + @pytest.mark.parametrize("off", ["BQ", "QS", "BQS"]) + def test_to_period_quarterlyish(self, off): + rng = date_range("01-Jan-2012", periods=8, freq=off) + prng = rng.to_period() + assert prng.freq == "Q-DEC" + + @pytest.mark.parametrize("off", ["BA", "AS", "BAS"]) + def test_to_period_annualish(self, off): + rng = date_range("01-Jan-2012", periods=8, freq=off) + prng = rng.to_period() + assert prng.freq == "A-DEC" + + def test_to_period_monthish(self): + offsets = ["MS", "BM"] + for off in offsets: + rng = date_range("01-Jan-2012", periods=8, freq=off) + prng = rng.to_period() + assert prng.freq == "M" + + rng = date_range("01-Jan-2012", periods=8, freq="M") + prng = rng.to_period() + assert prng.freq == "M" + + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + date_range("01-Jan-2012", periods=8, freq="EOM") + + def test_period_dt64_round_trip(self): + dti = date_range("1/1/2000", "1/7/2002", freq="B") + pi = dti.to_period() + tm.assert_index_equal(pi.to_timestamp(), dti) + + dti = date_range("1/1/2000", "1/7/2002", freq="B") + pi = dti.to_period(freq="H") + tm.assert_index_equal(pi.to_timestamp(), dti) + + def test_to_period_millisecond(self): + index = DatetimeIndex( + [ + Timestamp("2007-01-01 10:11:12.123456Z"), + Timestamp("2007-01-01 10:11:13.789123Z"), + ] + ) + + with tm.assert_produces_warning(UserWarning): + # warning that timezone info will be lost + period = index.to_period(freq="L") + assert 2 == len(period) + assert period[0] == Period("2007-01-01 10:11:12.123Z", "L") + assert period[1] == Period("2007-01-01 10:11:13.789Z", "L") + + def test_to_period_microsecond(self): + index = DatetimeIndex( + [ + Timestamp("2007-01-01 10:11:12.123456Z"), + Timestamp("2007-01-01 10:11:13.789123Z"), + ] + ) + + with tm.assert_produces_warning(UserWarning): + # warning that timezone info will be lost + period = index.to_period(freq="U") + assert 2 == len(period) + assert period[0] == Period("2007-01-01 10:11:12.123456Z", "U") + assert period[1] == Period("2007-01-01 10:11:13.789123Z", "U") + + @pytest.mark.parametrize( + "tz", + ["US/Eastern", pytz.utc, tzlocal(), "dateutil/US/Eastern", dateutil.tz.tzutc()], + ) + def test_to_period_tz(self, tz): + ts = date_range("1/1/2000", "2/1/2000", tz=tz) + + with tm.assert_produces_warning(UserWarning): + # GH#21333 warning that timezone info will be lost + result = ts.to_period()[0] + expected = ts[0].to_period() + + assert result == expected + + expected = date_range("1/1/2000", "2/1/2000").to_period() + + with tm.assert_produces_warning(UserWarning): + # GH#21333 warning that timezone info will be lost + result = ts.to_period() + + tm.assert_index_equal(result, expected) + + @pytest.mark.parametrize("tz", ["Etc/GMT-1", "Etc/GMT+1"]) + def test_to_period_tz_utc_offset_consistency(self, tz): + # GH#22905 + ts = date_range("1/1/2000", "2/1/2000", tz="Etc/GMT-1") + with tm.assert_produces_warning(UserWarning): + result = ts.to_period()[0] + expected = ts[0].to_period() + assert result == expected + + def test_to_period_nofreq(self): + idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-04"]) + with pytest.raises(ValueError): + idx.to_period() + + idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-03"], freq="infer") + assert idx.freqstr == "D" + expected = PeriodIndex(["2000-01-01", "2000-01-02", "2000-01-03"], freq="D") + tm.assert_index_equal(idx.to_period(), expected) + + # GH#7606 + idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-03"]) + assert idx.freqstr is None + tm.assert_index_equal(idx.to_period(), expected) diff --git a/pandas/tests/indexes/datetimes/test_tools.py b/pandas/tests/indexes/datetimes/test_tools.py index df3a49fb7c292..a91c837c9d9a2 100644 --- a/pandas/tests/indexes/datetimes/test_tools.py +++ b/pandas/tests/indexes/datetimes/test_tools.py @@ -2,7 +2,7 @@ import calendar from collections import deque -from datetime import datetime, time +from datetime import datetime, timedelta import locale from dateutil.parser import parse @@ -199,7 +199,7 @@ def test_to_datetime_format_microsecond(self, cache): # these are locale dependent lang, _ = locale.getlocale() month_abbr = calendar.month_abbr[4] - val = "01-{}-2011 00:00:01.978".format(month_abbr) + val = f"01-{month_abbr}-2011 00:00:01.978" format = "%d-%b-%Y %H:%M:%S.%f" result = to_datetime(val, format=format, cache=cache) @@ -551,7 +551,7 @@ def test_to_datetime_dt64s(self, cache): ) @pytest.mark.parametrize("cache", [True, False]) def test_to_datetime_dt64s_out_of_bounds(self, cache, dt): - msg = "Out of bounds nanosecond timestamp: {}".format(dt) + msg = f"Out of bounds nanosecond timestamp: {dt}" with pytest.raises(OutOfBoundsDatetime, match=msg): pd.to_datetime(dt, errors="raise") with pytest.raises(OutOfBoundsDatetime, match=msg): @@ -1110,8 +1110,8 @@ def test_unit(self, cache): for val in ["foo", Timestamp("20130101")]: try: to_datetime(val, errors="raise", unit="s", cache=cache) - except tslib.OutOfBoundsDatetime: - raise AssertionError("incorrect exception raised") + except tslib.OutOfBoundsDatetime as err: + raise AssertionError("incorrect exception raised") from err except ValueError: pass @@ -1376,6 +1376,86 @@ def test_to_datetime_errors_ignore_utc_true(self): expected = DatetimeIndex(["1970-01-01 00:00:01"], tz="UTC") tm.assert_index_equal(result, expected) + # TODO: this is moved from tests.series.test_timeseries, may be redundant + def test_to_datetime_unit(self): + + epoch = 1370745748 + s = Series([epoch + t for t in range(20)]) + result = to_datetime(s, unit="s") + expected = Series( + [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] + ) + tm.assert_series_equal(result, expected) + + s = Series([epoch + t for t in range(20)]).astype(float) + result = to_datetime(s, unit="s") + expected = Series( + [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] + ) + tm.assert_series_equal(result, expected) + + s = Series([epoch + t for t in range(20)] + [iNaT]) + result = to_datetime(s, unit="s") + expected = Series( + [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] + + [NaT] + ) + tm.assert_series_equal(result, expected) + + s = Series([epoch + t for t in range(20)] + [iNaT]).astype(float) + result = to_datetime(s, unit="s") + expected = Series( + [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] + + [NaT] + ) + tm.assert_series_equal(result, expected) + + # GH13834 + s = Series([epoch + t for t in np.arange(0, 2, 0.25)] + [iNaT]).astype(float) + result = to_datetime(s, unit="s") + expected = Series( + [ + Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) + for t in np.arange(0, 2, 0.25) + ] + + [NaT] + ) + tm.assert_series_equal(result, expected) + + s = pd.concat( + [Series([epoch + t for t in range(20)]).astype(float), Series([np.nan])], + ignore_index=True, + ) + result = to_datetime(s, unit="s") + expected = Series( + [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] + + [NaT] + ) + tm.assert_series_equal(result, expected) + + result = to_datetime([1, 2, "NaT", pd.NaT, np.nan], unit="D") + expected = DatetimeIndex( + [Timestamp("1970-01-02"), Timestamp("1970-01-03")] + ["NaT"] * 3 + ) + tm.assert_index_equal(result, expected) + + msg = "non convertible value foo with the unit 'D'" + with pytest.raises(ValueError, match=msg): + to_datetime([1, 2, "foo"], unit="D") + msg = "cannot convert input 111111111 with the unit 'D'" + with pytest.raises(OutOfBoundsDatetime, match=msg): + to_datetime([1, 2, 111111111], unit="D") + + # coerce we can process + expected = DatetimeIndex( + [Timestamp("1970-01-02"), Timestamp("1970-01-03")] + ["NaT"] * 1 + ) + result = to_datetime([1, 2, "foo"], unit="D", errors="coerce") + tm.assert_index_equal(result, expected) + + result = to_datetime([1, 2, 111111111], unit="D", errors="coerce") + tm.assert_index_equal(result, expected) + class TestToDatetimeMisc: def test_to_datetime_barely_out_of_bounds(self): @@ -2032,52 +2112,6 @@ def test_parsers_timestring(self, cache): assert result4 == exp_now assert result5 == exp_now - @td.skip_if_has_locale - def test_parsers_time(self): - # GH11818 - strings = [ - "14:15", - "1415", - "2:15pm", - "0215pm", - "14:15:00", - "141500", - "2:15:00pm", - "021500pm", - time(14, 15), - ] - expected = time(14, 15) - - for time_string in strings: - assert tools.to_time(time_string) == expected - - new_string = "14.15" - msg = r"Cannot convert arg \['14\.15'\] to a time" - with pytest.raises(ValueError, match=msg): - tools.to_time(new_string) - assert tools.to_time(new_string, format="%H.%M") == expected - - arg = ["14:15", "20:20"] - expected_arr = [time(14, 15), time(20, 20)] - assert tools.to_time(arg) == expected_arr - assert tools.to_time(arg, format="%H:%M") == expected_arr - assert tools.to_time(arg, infer_time_format=True) == expected_arr - assert tools.to_time(arg, format="%I:%M%p", errors="coerce") == [None, None] - - res = tools.to_time(arg, format="%I:%M%p", errors="ignore") - tm.assert_numpy_array_equal(res, np.array(arg, dtype=np.object_)) - - with pytest.raises(ValueError): - tools.to_time(arg, format="%I:%M%p", errors="raise") - - tm.assert_series_equal( - tools.to_time(Series(arg, name="test")), Series(expected_arr, name="test") - ) - - res = tools.to_time(np.array(arg)) - assert isinstance(res, list) - assert res == expected_arr - @pytest.mark.parametrize("cache", [True, False]) @pytest.mark.parametrize( "dt_string, tz, dt_string_repr", @@ -2315,3 +2349,10 @@ def test_nullable_integer_to_datetime(): tm.assert_series_equal(res, expected) # Check that ser isn't mutated tm.assert_series_equal(ser, ser_copy) + + +@pytest.mark.parametrize("klass", [np.array, list]) +def test_na_to_datetime(nulls_fixture, klass): + result = pd.to_datetime(klass([nulls_fixture])) + + assert result[0] is pd.NaT diff --git a/pandas/tests/indexes/interval/test_indexing.py b/pandas/tests/indexes/interval/test_indexing.py index 87b72f702e2aa..0e5721bfd83fd 100644 --- a/pandas/tests/indexes/interval/test_indexing.py +++ b/pandas/tests/indexes/interval/test_indexing.py @@ -24,11 +24,7 @@ def test_get_loc_interval(self, closed, side): for bound in [[0, 1], [1, 2], [2, 3], [3, 4], [0, 2], [2.5, 3], [-1, 4]]: # if get_loc is supplied an interval, it should only search # for exact matches, not overlaps or covers, else KeyError. - msg = re.escape( - "Interval({bound[0]}, {bound[1]}, closed='{side}')".format( - bound=bound, side=side - ) - ) + msg = re.escape(f"Interval({bound[0]}, {bound[1]}, closed='{side}')") if closed == side: if bound == [0, 1]: assert idx.get_loc(Interval(0, 1, closed=side)) == 0 @@ -86,11 +82,7 @@ def test_get_loc_length_one_interval(self, left, right, closed, other_closed): else: with pytest.raises( KeyError, - match=re.escape( - "Interval({left}, {right}, closed='{other_closed}')".format( - left=left, right=right, other_closed=other_closed - ) - ), + match=re.escape(f"Interval({left}, {right}, closed='{other_closed}')"), ): index.get_loc(interval) diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py index 47a0ba7fe0f21..c2b209c810af9 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -673,10 +673,7 @@ def test_append(self, closed): ) tm.assert_index_equal(result, expected) - msg = ( - "can only append two IntervalIndex objects that are closed " - "on the same side" - ) + msg = "Intervals must all be closed on the same side" for other_closed in {"left", "right", "both", "neither"} - {closed}: index_other_closed = IntervalIndex.from_arrays( [0, 1], [1, 2], closed=other_closed @@ -848,7 +845,7 @@ def test_set_closed(self, name, closed, new_closed): def test_set_closed_errors(self, bad_closed): # GH 21670 index = interval_range(0, 5) - msg = "invalid option for 'closed': {closed}".format(closed=bad_closed) + msg = f"invalid option for 'closed': {bad_closed}" with pytest.raises(ValueError, match=msg): index.set_closed(bad_closed) diff --git a/pandas/tests/indexes/interval/test_setops.py b/pandas/tests/indexes/interval/test_setops.py index 3246ac6bafde9..d9359d717de1d 100644 --- a/pandas/tests/indexes/interval/test_setops.py +++ b/pandas/tests/indexes/interval/test_setops.py @@ -180,8 +180,8 @@ def test_set_incompatible_types(self, closed, op_name, sort): # GH 19016: incompatible dtypes other = interval_range(Timestamp("20180101"), periods=9, closed=closed) msg = ( - "can only do {op} between two IntervalIndex objects that have " + f"can only do {op_name} between two IntervalIndex objects that have " "compatible dtypes" - ).format(op=op_name) + ) with pytest.raises(TypeError, match=msg): set_op(other, sort=sort) diff --git a/pandas/tests/indexes/multi/test_compat.py b/pandas/tests/indexes/multi/test_compat.py index 9a76f0623eb31..ef549beccda5d 100644 --- a/pandas/tests/indexes/multi/test_compat.py +++ b/pandas/tests/indexes/multi/test_compat.py @@ -29,7 +29,7 @@ def test_numeric_compat(idx): @pytest.mark.parametrize("method", ["all", "any"]) def test_logical_compat(idx, method): - msg = "cannot perform {method}".format(method=method) + msg = f"cannot perform {method}" with pytest.raises(TypeError, match=msg): getattr(idx, method)() diff --git a/pandas/tests/indexes/multi/test_copy.py b/pandas/tests/indexes/multi/test_copy.py index 1acc65aef8b8a..67b815ecba3b8 100644 --- a/pandas/tests/indexes/multi/test_copy.py +++ b/pandas/tests/indexes/multi/test_copy.py @@ -80,7 +80,6 @@ def test_copy_method_kwargs(deep, kwarg, value): codes=[[0, 0, 0, 1], [0, 0, 1, 1]], names=["first", "second"], ) - return idx_copy = idx.copy(**{kwarg: value, "deep": deep}) if kwarg == "names": assert getattr(idx_copy, kwarg) == value diff --git a/pandas/tests/indexes/multi/test_get_level_values.py b/pandas/tests/indexes/multi/test_get_level_values.py new file mode 100644 index 0000000000000..6f0b23c1ef4a0 --- /dev/null +++ b/pandas/tests/indexes/multi/test_get_level_values.py @@ -0,0 +1,13 @@ +from pandas import MultiIndex, Timestamp, date_range + + +class TestGetLevelValues: + def test_get_level_values_box_datetime64(self): + + dates = date_range("1/1/2000", periods=4) + levels = [dates, [0, 1]] + codes = [[0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 0, 1, 0, 1, 0, 1]] + + index = MultiIndex(levels=levels, codes=codes) + + assert isinstance(index.get_level_values(0)[0], Timestamp) diff --git a/pandas/tests/indexes/multi/test_indexing.py b/pandas/tests/indexes/multi/test_indexing.py index 21a4773fa3683..39049006edb7c 100644 --- a/pandas/tests/indexes/multi/test_indexing.py +++ b/pandas/tests/indexes/multi/test_indexing.py @@ -4,116 +4,126 @@ import pytest import pandas as pd -from pandas import ( - Categorical, - CategoricalIndex, - Index, - IntervalIndex, - MultiIndex, - date_range, -) +from pandas import Categorical, Index, MultiIndex, date_range import pandas._testing as tm from pandas.core.indexes.base import InvalidIndexError -def test_slice_locs_partial(idx): - sorted_idx, _ = idx.sortlevel(0) - - result = sorted_idx.slice_locs(("foo", "two"), ("qux", "one")) - assert result == (1, 5) +class TestSliceLocs: + def test_slice_locs_partial(self, idx): + sorted_idx, _ = idx.sortlevel(0) - result = sorted_idx.slice_locs(None, ("qux", "one")) - assert result == (0, 5) + result = sorted_idx.slice_locs(("foo", "two"), ("qux", "one")) + assert result == (1, 5) - result = sorted_idx.slice_locs(("foo", "two"), None) - assert result == (1, len(sorted_idx)) + result = sorted_idx.slice_locs(None, ("qux", "one")) + assert result == (0, 5) - result = sorted_idx.slice_locs("bar", "baz") - assert result == (2, 4) + result = sorted_idx.slice_locs(("foo", "two"), None) + assert result == (1, len(sorted_idx)) + result = sorted_idx.slice_locs("bar", "baz") + assert result == (2, 4) -def test_slice_locs(): - df = tm.makeTimeDataFrame() - stacked = df.stack() - idx = stacked.index + def test_slice_locs(self): + df = tm.makeTimeDataFrame() + stacked = df.stack() + idx = stacked.index - slob = slice(*idx.slice_locs(df.index[5], df.index[15])) - sliced = stacked[slob] - expected = df[5:16].stack() - tm.assert_almost_equal(sliced.values, expected.values) + slob = slice(*idx.slice_locs(df.index[5], df.index[15])) + sliced = stacked[slob] + expected = df[5:16].stack() + tm.assert_almost_equal(sliced.values, expected.values) - slob = slice( - *idx.slice_locs( - df.index[5] + timedelta(seconds=30), df.index[15] - timedelta(seconds=30) + slob = slice( + *idx.slice_locs( + df.index[5] + timedelta(seconds=30), + df.index[15] - timedelta(seconds=30), + ) ) - ) - sliced = stacked[slob] - expected = df[6:15].stack() - tm.assert_almost_equal(sliced.values, expected.values) - - -def test_slice_locs_with_type_mismatch(): - df = tm.makeTimeDataFrame() - stacked = df.stack() - idx = stacked.index - with pytest.raises(TypeError, match="^Level type mismatch"): - idx.slice_locs((1, 3)) - with pytest.raises(TypeError, match="^Level type mismatch"): - idx.slice_locs(df.index[5] + timedelta(seconds=30), (5, 2)) - df = tm.makeCustomDataframe(5, 5) - stacked = df.stack() - idx = stacked.index - with pytest.raises(TypeError, match="^Level type mismatch"): - idx.slice_locs(timedelta(seconds=30)) - # TODO: Try creating a UnicodeDecodeError in exception message - with pytest.raises(TypeError, match="^Level type mismatch"): - idx.slice_locs(df.index[1], (16, "a")) - - -def test_slice_locs_not_sorted(): - index = MultiIndex( - levels=[Index(np.arange(4)), Index(np.arange(4)), Index(np.arange(4))], - codes=[ - np.array([0, 0, 1, 2, 2, 2, 3, 3]), - np.array([0, 1, 0, 0, 0, 1, 0, 1]), - np.array([1, 0, 1, 1, 0, 0, 1, 0]), - ], - ) - msg = "[Kk]ey length.*greater than MultiIndex lexsort depth" - with pytest.raises(KeyError, match=msg): - index.slice_locs((1, 0, 1), (2, 1, 0)) + sliced = stacked[slob] + expected = df[6:15].stack() + tm.assert_almost_equal(sliced.values, expected.values) + + def test_slice_locs_with_type_mismatch(self): + df = tm.makeTimeDataFrame() + stacked = df.stack() + idx = stacked.index + with pytest.raises(TypeError, match="^Level type mismatch"): + idx.slice_locs((1, 3)) + with pytest.raises(TypeError, match="^Level type mismatch"): + idx.slice_locs(df.index[5] + timedelta(seconds=30), (5, 2)) + df = tm.makeCustomDataframe(5, 5) + stacked = df.stack() + idx = stacked.index + with pytest.raises(TypeError, match="^Level type mismatch"): + idx.slice_locs(timedelta(seconds=30)) + # TODO: Try creating a UnicodeDecodeError in exception message + with pytest.raises(TypeError, match="^Level type mismatch"): + idx.slice_locs(df.index[1], (16, "a")) + + def test_slice_locs_not_sorted(self): + index = MultiIndex( + levels=[Index(np.arange(4)), Index(np.arange(4)), Index(np.arange(4))], + codes=[ + np.array([0, 0, 1, 2, 2, 2, 3, 3]), + np.array([0, 1, 0, 0, 0, 1, 0, 1]), + np.array([1, 0, 1, 1, 0, 0, 1, 0]), + ], + ) + msg = "[Kk]ey length.*greater than MultiIndex lexsort depth" + with pytest.raises(KeyError, match=msg): + index.slice_locs((1, 0, 1), (2, 1, 0)) - # works - sorted_index, _ = index.sortlevel(0) - # should there be a test case here??? - sorted_index.slice_locs((1, 0, 1), (2, 1, 0)) + # works + sorted_index, _ = index.sortlevel(0) + # should there be a test case here??? + sorted_index.slice_locs((1, 0, 1), (2, 1, 0)) + def test_slice_locs_not_contained(self): + # some searchsorted action -def test_slice_locs_not_contained(): - # some searchsorted action + index = MultiIndex( + levels=[[0, 2, 4, 6], [0, 2, 4]], + codes=[[0, 0, 0, 1, 1, 2, 3, 3, 3], [0, 1, 2, 1, 2, 2, 0, 1, 2]], + ) - index = MultiIndex( - levels=[[0, 2, 4, 6], [0, 2, 4]], - codes=[[0, 0, 0, 1, 1, 2, 3, 3, 3], [0, 1, 2, 1, 2, 2, 0, 1, 2]], - ) + result = index.slice_locs((1, 0), (5, 2)) + assert result == (3, 6) - result = index.slice_locs((1, 0), (5, 2)) - assert result == (3, 6) + result = index.slice_locs(1, 5) + assert result == (3, 6) - result = index.slice_locs(1, 5) - assert result == (3, 6) + result = index.slice_locs((2, 2), (5, 2)) + assert result == (3, 6) - result = index.slice_locs((2, 2), (5, 2)) - assert result == (3, 6) + result = index.slice_locs(2, 5) + assert result == (3, 6) - result = index.slice_locs(2, 5) - assert result == (3, 6) + result = index.slice_locs((1, 0), (6, 3)) + assert result == (3, 8) - result = index.slice_locs((1, 0), (6, 3)) - assert result == (3, 8) + result = index.slice_locs(-1, 10) + assert result == (0, len(index)) - result = index.slice_locs(-1, 10) - assert result == (0, len(index)) + @pytest.mark.parametrize( + "index_arr,expected,start_idx,end_idx", + [ + ([[np.nan, "a", "b"], ["c", "d", "e"]], (0, 3), np.nan, None), + ([[np.nan, "a", "b"], ["c", "d", "e"]], (0, 3), np.nan, "b"), + ([[np.nan, "a", "b"], ["c", "d", "e"]], (0, 3), np.nan, ("b", "e")), + ([["a", "b", "c"], ["d", np.nan, "e"]], (1, 3), ("b", np.nan), None), + ([["a", "b", "c"], ["d", np.nan, "e"]], (1, 3), ("b", np.nan), "c"), + ([["a", "b", "c"], ["d", np.nan, "e"]], (1, 3), ("b", np.nan), ("c", "e")), + ], + ) + def test_slice_locs_with_missing_value( + self, index_arr, expected, start_idx, end_idx + ): + # issue 19132 + idx = MultiIndex.from_arrays(index_arr) + result = idx.slice_locs(start=start_idx, end=end_idx) + assert result == expected def test_putmask_with_wrong_mask(idx): @@ -130,67 +140,104 @@ def test_putmask_with_wrong_mask(idx): idx.putmask("foo", 1) -def test_get_indexer(): - major_axis = Index(np.arange(4)) - minor_axis = Index(np.arange(2)) +class TestGetIndexer: + def test_get_indexer(self): + major_axis = Index(np.arange(4)) + minor_axis = Index(np.arange(2)) - major_codes = np.array([0, 0, 1, 2, 2, 3, 3], dtype=np.intp) - minor_codes = np.array([0, 1, 0, 0, 1, 0, 1], dtype=np.intp) + major_codes = np.array([0, 0, 1, 2, 2, 3, 3], dtype=np.intp) + minor_codes = np.array([0, 1, 0, 0, 1, 0, 1], dtype=np.intp) - index = MultiIndex( - levels=[major_axis, minor_axis], codes=[major_codes, minor_codes] - ) - idx1 = index[:5] - idx2 = index[[1, 3, 5]] + index = MultiIndex( + levels=[major_axis, minor_axis], codes=[major_codes, minor_codes] + ) + idx1 = index[:5] + idx2 = index[[1, 3, 5]] - r1 = idx1.get_indexer(idx2) - tm.assert_almost_equal(r1, np.array([1, 3, -1], dtype=np.intp)) + r1 = idx1.get_indexer(idx2) + tm.assert_almost_equal(r1, np.array([1, 3, -1], dtype=np.intp)) - r1 = idx2.get_indexer(idx1, method="pad") - e1 = np.array([-1, 0, 0, 1, 1], dtype=np.intp) - tm.assert_almost_equal(r1, e1) + r1 = idx2.get_indexer(idx1, method="pad") + e1 = np.array([-1, 0, 0, 1, 1], dtype=np.intp) + tm.assert_almost_equal(r1, e1) - r2 = idx2.get_indexer(idx1[::-1], method="pad") - tm.assert_almost_equal(r2, e1[::-1]) + r2 = idx2.get_indexer(idx1[::-1], method="pad") + tm.assert_almost_equal(r2, e1[::-1]) - rffill1 = idx2.get_indexer(idx1, method="ffill") - tm.assert_almost_equal(r1, rffill1) + rffill1 = idx2.get_indexer(idx1, method="ffill") + tm.assert_almost_equal(r1, rffill1) - r1 = idx2.get_indexer(idx1, method="backfill") - e1 = np.array([0, 0, 1, 1, 2], dtype=np.intp) - tm.assert_almost_equal(r1, e1) + r1 = idx2.get_indexer(idx1, method="backfill") + e1 = np.array([0, 0, 1, 1, 2], dtype=np.intp) + tm.assert_almost_equal(r1, e1) - r2 = idx2.get_indexer(idx1[::-1], method="backfill") - tm.assert_almost_equal(r2, e1[::-1]) + r2 = idx2.get_indexer(idx1[::-1], method="backfill") + tm.assert_almost_equal(r2, e1[::-1]) - rbfill1 = idx2.get_indexer(idx1, method="bfill") - tm.assert_almost_equal(r1, rbfill1) + rbfill1 = idx2.get_indexer(idx1, method="bfill") + tm.assert_almost_equal(r1, rbfill1) - # pass non-MultiIndex - r1 = idx1.get_indexer(idx2.values) - rexp1 = idx1.get_indexer(idx2) - tm.assert_almost_equal(r1, rexp1) + # pass non-MultiIndex + r1 = idx1.get_indexer(idx2.values) + rexp1 = idx1.get_indexer(idx2) + tm.assert_almost_equal(r1, rexp1) - r1 = idx1.get_indexer([1, 2, 3]) - assert (r1 == [-1, -1, -1]).all() + r1 = idx1.get_indexer([1, 2, 3]) + assert (r1 == [-1, -1, -1]).all() - # create index with duplicates - idx1 = Index(list(range(10)) + list(range(10))) - idx2 = Index(list(range(20))) + # create index with duplicates + idx1 = Index(list(range(10)) + list(range(10))) + idx2 = Index(list(range(20))) - msg = "Reindexing only valid with uniquely valued Index objects" - with pytest.raises(InvalidIndexError, match=msg): - idx1.get_indexer(idx2) + msg = "Reindexing only valid with uniquely valued Index objects" + with pytest.raises(InvalidIndexError, match=msg): + idx1.get_indexer(idx2) + def test_get_indexer_nearest(self): + midx = MultiIndex.from_tuples([("a", 1), ("b", 2)]) + msg = ( + "method='nearest' not implemented yet for MultiIndex; " + "see GitHub issue 9365" + ) + with pytest.raises(NotImplementedError, match=msg): + midx.get_indexer(["a"], method="nearest") + msg = "tolerance not implemented yet for MultiIndex" + with pytest.raises(NotImplementedError, match=msg): + midx.get_indexer(["a"], method="pad", tolerance=2) + + def test_get_indexer_categorical_time(self): + # https://github.com/pandas-dev/pandas/issues/21390 + midx = MultiIndex.from_product( + [ + Categorical(["a", "b", "c"]), + Categorical(date_range("2012-01-01", periods=3, freq="H")), + ] + ) + result = midx.get_indexer(midx) + tm.assert_numpy_array_equal(result, np.arange(9, dtype=np.intp)) -def test_get_indexer_nearest(): - midx = MultiIndex.from_tuples([("a", 1), ("b", 2)]) - msg = "method='nearest' not implemented yet for MultiIndex; see GitHub issue 9365" - with pytest.raises(NotImplementedError, match=msg): - midx.get_indexer(["a"], method="nearest") - msg = "tolerance not implemented yet for MultiIndex" - with pytest.raises(NotImplementedError, match=msg): - midx.get_indexer(["a"], method="pad", tolerance=2) + @pytest.mark.parametrize( + "index_arr,labels,expected", + [ + ( + [[1, np.nan, 2], [3, 4, 5]], + [1, np.nan, 2], + np.array([-1, -1, -1], dtype=np.intp), + ), + ([[1, np.nan, 2], [3, 4, 5]], [(np.nan, 4)], np.array([1], dtype=np.intp)), + ([[1, 2, 3], [np.nan, 4, 5]], [(1, np.nan)], np.array([0], dtype=np.intp)), + ( + [[1, 2, 3], [np.nan, 4, 5]], + [np.nan, 4, 5], + np.array([-1, -1, -1], dtype=np.intp), + ), + ], + ) + def test_get_indexer_with_missing_value(self, index_arr, labels, expected): + # issue 19132 + idx = MultiIndex.from_arrays(index_arr) + result = idx.get_indexer(labels) + tm.assert_numpy_array_equal(result, expected) def test_getitem(idx): @@ -216,25 +263,6 @@ def test_getitem_group_select(idx): assert sorted_idx.get_loc("foo") == slice(0, 2) -def test_get_indexer_consistency(idx): - # See GH 16819 - if isinstance(idx, IntervalIndex): - pass - - if idx.is_unique or isinstance(idx, CategoricalIndex): - indexer = idx.get_indexer(idx[0:2]) - assert isinstance(indexer, np.ndarray) - assert indexer.dtype == np.intp - else: - e = "Reindexing only valid with uniquely valued Index objects" - with pytest.raises(InvalidIndexError, match=e): - idx.get_indexer(idx[0:2]) - - indexer, _ = idx.get_indexer_non_unique(idx[0:2]) - assert isinstance(indexer, np.ndarray) - assert indexer.dtype == np.intp - - @pytest.mark.parametrize("ind1", [[True] * 5, pd.Index([True] * 5)]) @pytest.mark.parametrize( "ind2", @@ -263,154 +291,155 @@ def test_getitem_bool_index_single(ind1, ind2): tm.assert_index_equal(idx[ind2], expected) -def test_get_loc(idx): - assert idx.get_loc(("foo", "two")) == 1 - assert idx.get_loc(("baz", "two")) == 3 - with pytest.raises(KeyError, match=r"^10$"): - idx.get_loc(("bar", "two")) - with pytest.raises(KeyError, match=r"^'quux'$"): - idx.get_loc("quux") - - msg = "only the default get_loc method is currently supported for MultiIndex" - with pytest.raises(NotImplementedError, match=msg): - idx.get_loc("foo", method="nearest") - - # 3 levels - index = MultiIndex( - levels=[Index(np.arange(4)), Index(np.arange(4)), Index(np.arange(4))], - codes=[ - np.array([0, 0, 1, 2, 2, 2, 3, 3]), - np.array([0, 1, 0, 0, 0, 1, 0, 1]), - np.array([1, 0, 1, 1, 0, 0, 1, 0]), - ], - ) - with pytest.raises(KeyError, match=r"^\(1, 1\)$"): - index.get_loc((1, 1)) - assert index.get_loc((2, 0)) == slice(3, 5) - - -def test_get_loc_duplicates(): - index = Index([2, 2, 2, 2]) - result = index.get_loc(2) - expected = slice(0, 4) - assert result == expected - # pytest.raises(Exception, index.get_loc, 2) - - index = Index(["c", "a", "a", "b", "b"]) - rs = index.get_loc("c") - xp = 0 - assert rs == xp - - -def test_get_loc_level(): - index = MultiIndex( - levels=[Index(np.arange(4)), Index(np.arange(4)), Index(np.arange(4))], - codes=[ - np.array([0, 0, 1, 2, 2, 2, 3, 3]), - np.array([0, 1, 0, 0, 0, 1, 0, 1]), - np.array([1, 0, 1, 1, 0, 0, 1, 0]), - ], - ) - loc, new_index = index.get_loc_level((0, 1)) - expected = slice(1, 2) - exp_index = index[expected].droplevel(0).droplevel(0) - assert loc == expected - assert new_index.equals(exp_index) - - loc, new_index = index.get_loc_level((0, 1, 0)) - expected = 1 - assert loc == expected - assert new_index is None - - with pytest.raises(KeyError, match=r"^\(2, 2\)$"): - index.get_loc_level((2, 2)) - # GH 22221: unused label - with pytest.raises(KeyError, match=r"^2$"): - index.drop(2).get_loc_level(2) - # Unused label on unsorted level: - with pytest.raises(KeyError, match=r"^2$"): - index.drop(1, level=2).get_loc_level(2, level=2) - - index = MultiIndex( - levels=[[2000], list(range(4))], - codes=[np.array([0, 0, 0, 0]), np.array([0, 1, 2, 3])], - ) - result, new_index = index.get_loc_level((2000, slice(None, None))) - expected = slice(None, None) - assert result == expected - assert new_index.equals(index.droplevel(0)) - - -@pytest.mark.parametrize("dtype1", [int, float, bool, str]) -@pytest.mark.parametrize("dtype2", [int, float, bool, str]) -def test_get_loc_multiple_dtypes(dtype1, dtype2): - # GH 18520 - levels = [np.array([0, 1]).astype(dtype1), np.array([0, 1]).astype(dtype2)] - idx = pd.MultiIndex.from_product(levels) - assert idx.get_loc(idx[2]) == 2 - - -@pytest.mark.parametrize("level", [0, 1]) -@pytest.mark.parametrize("dtypes", [[int, float], [float, int]]) -def test_get_loc_implicit_cast(level, dtypes): - # GH 18818, GH 15994 : as flat index, cast int to float and vice-versa - levels = [["a", "b"], ["c", "d"]] - key = ["b", "d"] - lev_dtype, key_dtype = dtypes - levels[level] = np.array([0, 1], dtype=lev_dtype) - key[level] = key_dtype(1) - idx = MultiIndex.from_product(levels) - assert idx.get_loc(tuple(key)) == 3 - - -def test_get_loc_cast_bool(): - # GH 19086 : int is casted to bool, but not vice-versa - levels = [[False, True], np.arange(2, dtype="int64")] - idx = MultiIndex.from_product(levels) - - assert idx.get_loc((0, 1)) == 1 - assert idx.get_loc((1, 0)) == 2 - - with pytest.raises(KeyError, match=r"^\(False, True\)$"): - idx.get_loc((False, True)) - with pytest.raises(KeyError, match=r"^\(True, False\)$"): - idx.get_loc((True, False)) - - -@pytest.mark.parametrize("level", [0, 1]) -def test_get_loc_nan(level, nulls_fixture): - # GH 18485 : NaN in MultiIndex - levels = [["a", "b"], ["c", "d"]] - key = ["b", "d"] - levels[level] = np.array([0, nulls_fixture], dtype=type(nulls_fixture)) - key[level] = nulls_fixture - idx = MultiIndex.from_product(levels) - assert idx.get_loc(tuple(key)) == 3 - - -def test_get_loc_missing_nan(): - # GH 8569 - idx = MultiIndex.from_arrays([[1.0, 2.0], [3.0, 4.0]]) - assert isinstance(idx.get_loc(1), slice) - with pytest.raises(KeyError, match=r"^3$"): - idx.get_loc(3) - with pytest.raises(KeyError, match=r"^nan$"): - idx.get_loc(np.nan) - with pytest.raises(TypeError, match="unhashable type: 'list'"): - # listlike/non-hashable raises TypeError - idx.get_loc([np.nan]) - - -def test_get_indexer_categorical_time(): - # https://github.com/pandas-dev/pandas/issues/21390 - midx = MultiIndex.from_product( - [ - Categorical(["a", "b", "c"]), - Categorical(date_range("2012-01-01", periods=3, freq="H")), - ] - ) - result = midx.get_indexer(midx) - tm.assert_numpy_array_equal(result, np.arange(9, dtype=np.intp)) +class TestGetLoc: + def test_get_loc(self, idx): + assert idx.get_loc(("foo", "two")) == 1 + assert idx.get_loc(("baz", "two")) == 3 + with pytest.raises(KeyError, match=r"^10$"): + idx.get_loc(("bar", "two")) + with pytest.raises(KeyError, match=r"^'quux'$"): + idx.get_loc("quux") + + msg = "only the default get_loc method is currently supported for MultiIndex" + with pytest.raises(NotImplementedError, match=msg): + idx.get_loc("foo", method="nearest") + + # 3 levels + index = MultiIndex( + levels=[Index(np.arange(4)), Index(np.arange(4)), Index(np.arange(4))], + codes=[ + np.array([0, 0, 1, 2, 2, 2, 3, 3]), + np.array([0, 1, 0, 0, 0, 1, 0, 1]), + np.array([1, 0, 1, 1, 0, 0, 1, 0]), + ], + ) + with pytest.raises(KeyError, match=r"^\(1, 1\)$"): + index.get_loc((1, 1)) + assert index.get_loc((2, 0)) == slice(3, 5) + + def test_get_loc_duplicates(self): + index = Index([2, 2, 2, 2]) + result = index.get_loc(2) + expected = slice(0, 4) + assert result == expected + # FIXME: dont leave commented-out + # pytest.raises(Exception, index.get_loc, 2) + + index = Index(["c", "a", "a", "b", "b"]) + rs = index.get_loc("c") + xp = 0 + assert rs == xp + + def test_get_loc_level(self): + index = MultiIndex( + levels=[Index(np.arange(4)), Index(np.arange(4)), Index(np.arange(4))], + codes=[ + np.array([0, 0, 1, 2, 2, 2, 3, 3]), + np.array([0, 1, 0, 0, 0, 1, 0, 1]), + np.array([1, 0, 1, 1, 0, 0, 1, 0]), + ], + ) + loc, new_index = index.get_loc_level((0, 1)) + expected = slice(1, 2) + exp_index = index[expected].droplevel(0).droplevel(0) + assert loc == expected + assert new_index.equals(exp_index) + + loc, new_index = index.get_loc_level((0, 1, 0)) + expected = 1 + assert loc == expected + assert new_index is None + + with pytest.raises(KeyError, match=r"^\(2, 2\)$"): + index.get_loc_level((2, 2)) + # GH 22221: unused label + with pytest.raises(KeyError, match=r"^2$"): + index.drop(2).get_loc_level(2) + # Unused label on unsorted level: + with pytest.raises(KeyError, match=r"^2$"): + index.drop(1, level=2).get_loc_level(2, level=2) + + index = MultiIndex( + levels=[[2000], list(range(4))], + codes=[np.array([0, 0, 0, 0]), np.array([0, 1, 2, 3])], + ) + result, new_index = index.get_loc_level((2000, slice(None, None))) + expected = slice(None, None) + assert result == expected + assert new_index.equals(index.droplevel(0)) + + @pytest.mark.parametrize("dtype1", [int, float, bool, str]) + @pytest.mark.parametrize("dtype2", [int, float, bool, str]) + def test_get_loc_multiple_dtypes(self, dtype1, dtype2): + # GH 18520 + levels = [np.array([0, 1]).astype(dtype1), np.array([0, 1]).astype(dtype2)] + idx = pd.MultiIndex.from_product(levels) + assert idx.get_loc(idx[2]) == 2 + + @pytest.mark.parametrize("level", [0, 1]) + @pytest.mark.parametrize("dtypes", [[int, float], [float, int]]) + def test_get_loc_implicit_cast(self, level, dtypes): + # GH 18818, GH 15994 : as flat index, cast int to float and vice-versa + levels = [["a", "b"], ["c", "d"]] + key = ["b", "d"] + lev_dtype, key_dtype = dtypes + levels[level] = np.array([0, 1], dtype=lev_dtype) + key[level] = key_dtype(1) + idx = MultiIndex.from_product(levels) + assert idx.get_loc(tuple(key)) == 3 + + def test_get_loc_cast_bool(self): + # GH 19086 : int is casted to bool, but not vice-versa + levels = [[False, True], np.arange(2, dtype="int64")] + idx = MultiIndex.from_product(levels) + + assert idx.get_loc((0, 1)) == 1 + assert idx.get_loc((1, 0)) == 2 + + with pytest.raises(KeyError, match=r"^\(False, True\)$"): + idx.get_loc((False, True)) + with pytest.raises(KeyError, match=r"^\(True, False\)$"): + idx.get_loc((True, False)) + + @pytest.mark.parametrize("level", [0, 1]) + def test_get_loc_nan(self, level, nulls_fixture): + # GH 18485 : NaN in MultiIndex + levels = [["a", "b"], ["c", "d"]] + key = ["b", "d"] + levels[level] = np.array([0, nulls_fixture], dtype=type(nulls_fixture)) + key[level] = nulls_fixture + + if nulls_fixture is pd.NA: + pytest.xfail("MultiIndex from pd.NA in np.array broken; see GH 31883") + + idx = MultiIndex.from_product(levels) + assert idx.get_loc(tuple(key)) == 3 + + def test_get_loc_missing_nan(self): + # GH 8569 + idx = MultiIndex.from_arrays([[1.0, 2.0], [3.0, 4.0]]) + assert isinstance(idx.get_loc(1), slice) + with pytest.raises(KeyError, match=r"^3$"): + idx.get_loc(3) + with pytest.raises(KeyError, match=r"^nan$"): + idx.get_loc(np.nan) + with pytest.raises(TypeError, match="unhashable type: 'list'"): + # listlike/non-hashable raises TypeError + idx.get_loc([np.nan]) + + def test_get_loc_with_values_including_missing_values(self): + # issue 19132 + idx = MultiIndex.from_product([[np.nan, 1]] * 2) + expected = slice(0, 2, None) + assert idx.get_loc(np.nan) == expected + + idx = MultiIndex.from_arrays([[np.nan, 1, 2, np.nan]]) + expected = np.array([True, False, False, True]) + tm.assert_numpy_array_equal(idx.get_loc(np.nan), expected) + + idx = MultiIndex.from_product([[np.nan, 1]] * 3) + expected = slice(2, 4, None) + assert idx.get_loc((np.nan, 1)) == expected def test_timestamp_multiindex_indexer(): @@ -440,45 +469,6 @@ def test_timestamp_multiindex_indexer(): tm.assert_series_equal(result, should_be) -def test_get_loc_with_values_including_missing_values(): - # issue 19132 - idx = MultiIndex.from_product([[np.nan, 1]] * 2) - expected = slice(0, 2, None) - assert idx.get_loc(np.nan) == expected - - idx = MultiIndex.from_arrays([[np.nan, 1, 2, np.nan]]) - expected = np.array([True, False, False, True]) - tm.assert_numpy_array_equal(idx.get_loc(np.nan), expected) - - idx = MultiIndex.from_product([[np.nan, 1]] * 3) - expected = slice(2, 4, None) - assert idx.get_loc((np.nan, 1)) == expected - - -@pytest.mark.parametrize( - "index_arr,labels,expected", - [ - ( - [[1, np.nan, 2], [3, 4, 5]], - [1, np.nan, 2], - np.array([-1, -1, -1], dtype=np.intp), - ), - ([[1, np.nan, 2], [3, 4, 5]], [(np.nan, 4)], np.array([1], dtype=np.intp)), - ([[1, 2, 3], [np.nan, 4, 5]], [(1, np.nan)], np.array([0], dtype=np.intp)), - ( - [[1, 2, 3], [np.nan, 4, 5]], - [np.nan, 4, 5], - np.array([-1, -1, -1], dtype=np.intp), - ), - ], -) -def test_get_indexer_with_missing_value(index_arr, labels, expected): - # issue 19132 - idx = MultiIndex.from_arrays(index_arr) - result = idx.get_indexer(labels) - tm.assert_numpy_array_equal(result, expected) - - @pytest.mark.parametrize( "index_arr,expected,target,algo", [ @@ -508,21 +498,3 @@ def test_slice_indexer_with_missing_value(index_arr, expected, start_idx, end_id idx = MultiIndex.from_arrays(index_arr) result = idx.slice_indexer(start=start_idx, end=end_idx) assert result == expected - - -@pytest.mark.parametrize( - "index_arr,expected,start_idx,end_idx", - [ - ([[np.nan, "a", "b"], ["c", "d", "e"]], (0, 3), np.nan, None), - ([[np.nan, "a", "b"], ["c", "d", "e"]], (0, 3), np.nan, "b"), - ([[np.nan, "a", "b"], ["c", "d", "e"]], (0, 3), np.nan, ("b", "e")), - ([["a", "b", "c"], ["d", np.nan, "e"]], (1, 3), ("b", np.nan), None), - ([["a", "b", "c"], ["d", np.nan, "e"]], (1, 3), ("b", np.nan), "c"), - ([["a", "b", "c"], ["d", np.nan, "e"]], (1, 3), ("b", np.nan), ("c", "e")), - ], -) -def test_slice_locs_with_missing_value(index_arr, expected, start_idx, end_idx): - # issue 19132 - idx = MultiIndex.from_arrays(index_arr) - result = idx.slice_locs(start=start_idx, end=end_idx) - assert result == expected diff --git a/pandas/tests/indexes/multi/test_setops.py b/pandas/tests/indexes/multi/test_setops.py index f949db537de67..627127f7b5b53 100644 --- a/pandas/tests/indexes/multi/test_setops.py +++ b/pandas/tests/indexes/multi/test_setops.py @@ -19,22 +19,20 @@ def test_set_ops_error_cases(idx, case, sort, method): @pytest.mark.parametrize("sort", [None, False]) -def test_intersection_base(idx, sort): - first = idx[:5] - second = idx[:3] - intersect = first.intersection(second, sort=sort) +@pytest.mark.parametrize("klass", [MultiIndex, np.array, Series, list]) +def test_intersection_base(idx, sort, klass): + first = idx[2::-1] # first 3 elements reversed + second = idx[:5] - if sort is None: - tm.assert_index_equal(intersect, second.sort_values()) - assert tm.equalContents(intersect, second) + if klass is not MultiIndex: + second = klass(second.values) - # GH 10149 - cases = [klass(second.values) for klass in [np.array, Series, list]] - for case in cases: - result = first.intersection(case, sort=sort) - if sort is None: - tm.assert_index_equal(result, second.sort_values()) - assert tm.equalContents(result, second) + intersect = first.intersection(second, sort=sort) + if sort is None: + expected = first.sort_values() + else: + expected = first + tm.assert_index_equal(intersect, expected) msg = "other must be a MultiIndex or a list of tuples" with pytest.raises(TypeError, match=msg): @@ -42,22 +40,20 @@ def test_intersection_base(idx, sort): @pytest.mark.parametrize("sort", [None, False]) -def test_union_base(idx, sort): - first = idx[3:] +@pytest.mark.parametrize("klass", [MultiIndex, np.array, Series, list]) +def test_union_base(idx, sort, klass): + first = idx[::-1] second = idx[:5] - everything = idx + + if klass is not MultiIndex: + second = klass(second.values) + union = first.union(second, sort=sort) if sort is None: - tm.assert_index_equal(union, everything.sort_values()) - assert tm.equalContents(union, everything) - - # GH 10149 - cases = [klass(second.values) for klass in [np.array, Series, list]] - for case in cases: - result = first.union(case, sort=sort) - if sort is None: - tm.assert_index_equal(result, everything.sort_values()) - assert tm.equalContents(result, everything) + expected = first.sort_values() + else: + expected = first + tm.assert_index_equal(union, expected) msg = "other must be a MultiIndex or a list of tuples" with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/indexes/numeric/__init__.py b/pandas/tests/indexes/numeric/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/indexes/numeric/test_join.py b/pandas/tests/indexes/numeric/test_join.py new file mode 100644 index 0000000000000..c8dffa411e5fd --- /dev/null +++ b/pandas/tests/indexes/numeric/test_join.py @@ -0,0 +1,388 @@ +import numpy as np +import pytest + +from pandas import Index, Int64Index, UInt64Index +import pandas._testing as tm + + +class TestJoinInt64Index: + def test_join_non_unique(self): + left = Index([4, 4, 3, 3]) + + joined, lidx, ridx = left.join(left, return_indexers=True) + + exp_joined = Index([3, 3, 3, 3, 4, 4, 4, 4]) + tm.assert_index_equal(joined, exp_joined) + + exp_lidx = np.array([2, 2, 3, 3, 0, 0, 1, 1], dtype=np.intp) + tm.assert_numpy_array_equal(lidx, exp_lidx) + + exp_ridx = np.array([2, 3, 2, 3, 0, 1, 0, 1], dtype=np.intp) + tm.assert_numpy_array_equal(ridx, exp_ridx) + + def test_join_inner(self): + index = Int64Index(range(0, 20, 2)) + other = Int64Index([7, 12, 25, 1, 2, 5]) + other_mono = Int64Index([1, 2, 5, 7, 12, 25]) + + # not monotonic + res, lidx, ridx = index.join(other, how="inner", return_indexers=True) + + # no guarantee of sortedness, so sort for comparison purposes + ind = res.argsort() + res = res.take(ind) + lidx = lidx.take(ind) + ridx = ridx.take(ind) + + eres = Int64Index([2, 12]) + elidx = np.array([1, 6], dtype=np.intp) + eridx = np.array([4, 1], dtype=np.intp) + + assert isinstance(res, Int64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + # monotonic + res, lidx, ridx = index.join(other_mono, how="inner", return_indexers=True) + + res2 = index.intersection(other_mono) + tm.assert_index_equal(res, res2) + + elidx = np.array([1, 6], dtype=np.intp) + eridx = np.array([1, 4], dtype=np.intp) + assert isinstance(res, Int64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + def test_join_left(self): + index = Int64Index(range(0, 20, 2)) + other = Int64Index([7, 12, 25, 1, 2, 5]) + other_mono = Int64Index([1, 2, 5, 7, 12, 25]) + + # not monotonic + res, lidx, ridx = index.join(other, how="left", return_indexers=True) + eres = index + eridx = np.array([-1, 4, -1, -1, -1, -1, 1, -1, -1, -1], dtype=np.intp) + + assert isinstance(res, Int64Index) + tm.assert_index_equal(res, eres) + assert lidx is None + tm.assert_numpy_array_equal(ridx, eridx) + + # monotonic + res, lidx, ridx = index.join(other_mono, how="left", return_indexers=True) + eridx = np.array([-1, 1, -1, -1, -1, -1, 4, -1, -1, -1], dtype=np.intp) + assert isinstance(res, Int64Index) + tm.assert_index_equal(res, eres) + assert lidx is None + tm.assert_numpy_array_equal(ridx, eridx) + + # non-unique + idx = Index([1, 1, 2, 5]) + idx2 = Index([1, 2, 5, 7, 9]) + res, lidx, ridx = idx2.join(idx, how="left", return_indexers=True) + eres = Index([1, 1, 2, 5, 7, 9]) # 1 is in idx2, so it should be x2 + eridx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) + elidx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + def test_join_right(self): + index = Int64Index(range(0, 20, 2)) + other = Int64Index([7, 12, 25, 1, 2, 5]) + other_mono = Int64Index([1, 2, 5, 7, 12, 25]) + + # not monotonic + res, lidx, ridx = index.join(other, how="right", return_indexers=True) + eres = other + elidx = np.array([-1, 6, -1, -1, 1, -1], dtype=np.intp) + + assert isinstance(other, Int64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + assert ridx is None + + # monotonic + res, lidx, ridx = index.join(other_mono, how="right", return_indexers=True) + eres = other_mono + elidx = np.array([-1, 1, -1, -1, 6, -1], dtype=np.intp) + assert isinstance(other, Int64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + assert ridx is None + + # non-unique + idx = Index([1, 1, 2, 5]) + idx2 = Index([1, 2, 5, 7, 9]) + res, lidx, ridx = idx.join(idx2, how="right", return_indexers=True) + eres = Index([1, 1, 2, 5, 7, 9]) # 1 is in idx2, so it should be x2 + elidx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) + eridx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + def test_join_non_int_index(self): + index = Int64Index(range(0, 20, 2)) + other = Index([3, 6, 7, 8, 10], dtype=object) + + outer = index.join(other, how="outer") + outer2 = other.join(index, how="outer") + expected = Index([0, 2, 3, 4, 6, 7, 8, 10, 12, 14, 16, 18]) + tm.assert_index_equal(outer, outer2) + tm.assert_index_equal(outer, expected) + + inner = index.join(other, how="inner") + inner2 = other.join(index, how="inner") + expected = Index([6, 8, 10]) + tm.assert_index_equal(inner, inner2) + tm.assert_index_equal(inner, expected) + + left = index.join(other, how="left") + tm.assert_index_equal(left, index.astype(object)) + + left2 = other.join(index, how="left") + tm.assert_index_equal(left2, other) + + right = index.join(other, how="right") + tm.assert_index_equal(right, other) + + right2 = other.join(index, how="right") + tm.assert_index_equal(right2, index.astype(object)) + + def test_join_outer(self): + index = Int64Index(range(0, 20, 2)) + other = Int64Index([7, 12, 25, 1, 2, 5]) + other_mono = Int64Index([1, 2, 5, 7, 12, 25]) + + # not monotonic + # guarantee of sortedness + res, lidx, ridx = index.join(other, how="outer", return_indexers=True) + noidx_res = index.join(other, how="outer") + tm.assert_index_equal(res, noidx_res) + + eres = Int64Index([0, 1, 2, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 25]) + elidx = np.array([0, -1, 1, 2, -1, 3, -1, 4, 5, 6, 7, 8, 9, -1], dtype=np.intp) + eridx = np.array( + [-1, 3, 4, -1, 5, -1, 0, -1, -1, 1, -1, -1, -1, 2], dtype=np.intp + ) + + assert isinstance(res, Int64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + # monotonic + res, lidx, ridx = index.join(other_mono, how="outer", return_indexers=True) + noidx_res = index.join(other_mono, how="outer") + tm.assert_index_equal(res, noidx_res) + + elidx = np.array([0, -1, 1, 2, -1, 3, -1, 4, 5, 6, 7, 8, 9, -1], dtype=np.intp) + eridx = np.array( + [-1, 0, 1, -1, 2, -1, 3, -1, -1, 4, -1, -1, -1, 5], dtype=np.intp + ) + assert isinstance(res, Int64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + +class TestJoinUInt64Index: + @pytest.fixture + def index_large(self): + # large values used in TestUInt64Index where no compat needed with Int64/Float64 + large = [2 ** 63, 2 ** 63 + 10, 2 ** 63 + 15, 2 ** 63 + 20, 2 ** 63 + 25] + return UInt64Index(large) + + def test_join_inner(self, index_large): + other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) + other_mono = UInt64Index( + 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") + ) + + # not monotonic + res, lidx, ridx = index_large.join(other, how="inner", return_indexers=True) + + # no guarantee of sortedness, so sort for comparison purposes + ind = res.argsort() + res = res.take(ind) + lidx = lidx.take(ind) + ridx = ridx.take(ind) + + eres = UInt64Index(2 ** 63 + np.array([10, 25], dtype="uint64")) + elidx = np.array([1, 4], dtype=np.intp) + eridx = np.array([5, 2], dtype=np.intp) + + assert isinstance(res, UInt64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + # monotonic + res, lidx, ridx = index_large.join( + other_mono, how="inner", return_indexers=True + ) + + res2 = index_large.intersection(other_mono) + tm.assert_index_equal(res, res2) + + elidx = np.array([1, 4], dtype=np.intp) + eridx = np.array([3, 5], dtype=np.intp) + + assert isinstance(res, UInt64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + def test_join_left(self, index_large): + other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) + other_mono = UInt64Index( + 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") + ) + + # not monotonic + res, lidx, ridx = index_large.join(other, how="left", return_indexers=True) + eres = index_large + eridx = np.array([-1, 5, -1, -1, 2], dtype=np.intp) + + assert isinstance(res, UInt64Index) + tm.assert_index_equal(res, eres) + assert lidx is None + tm.assert_numpy_array_equal(ridx, eridx) + + # monotonic + res, lidx, ridx = index_large.join(other_mono, how="left", return_indexers=True) + eridx = np.array([-1, 3, -1, -1, 5], dtype=np.intp) + + assert isinstance(res, UInt64Index) + tm.assert_index_equal(res, eres) + assert lidx is None + tm.assert_numpy_array_equal(ridx, eridx) + + # non-unique + idx = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5], dtype="uint64")) + idx2 = UInt64Index(2 ** 63 + np.array([1, 2, 5, 7, 9], dtype="uint64")) + res, lidx, ridx = idx2.join(idx, how="left", return_indexers=True) + + # 1 is in idx2, so it should be x2 + eres = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5, 7, 9], dtype="uint64")) + eridx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) + elidx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) + + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + def test_join_right(self, index_large): + other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) + other_mono = UInt64Index( + 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") + ) + + # not monotonic + res, lidx, ridx = index_large.join(other, how="right", return_indexers=True) + eres = other + elidx = np.array([-1, -1, 4, -1, -1, 1], dtype=np.intp) + + tm.assert_numpy_array_equal(lidx, elidx) + assert isinstance(other, UInt64Index) + tm.assert_index_equal(res, eres) + assert ridx is None + + # monotonic + res, lidx, ridx = index_large.join( + other_mono, how="right", return_indexers=True + ) + eres = other_mono + elidx = np.array([-1, -1, -1, 1, -1, 4], dtype=np.intp) + + assert isinstance(other, UInt64Index) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_index_equal(res, eres) + assert ridx is None + + # non-unique + idx = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5], dtype="uint64")) + idx2 = UInt64Index(2 ** 63 + np.array([1, 2, 5, 7, 9], dtype="uint64")) + res, lidx, ridx = idx.join(idx2, how="right", return_indexers=True) + + # 1 is in idx2, so it should be x2 + eres = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5, 7, 9], dtype="uint64")) + elidx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) + eridx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) + + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + def test_join_non_int_index(self, index_large): + other = Index( + 2 ** 63 + np.array([1, 5, 7, 10, 20], dtype="uint64"), dtype=object + ) + + outer = index_large.join(other, how="outer") + outer2 = other.join(index_large, how="outer") + expected = Index( + 2 ** 63 + np.array([0, 1, 5, 7, 10, 15, 20, 25], dtype="uint64") + ) + tm.assert_index_equal(outer, outer2) + tm.assert_index_equal(outer, expected) + + inner = index_large.join(other, how="inner") + inner2 = other.join(index_large, how="inner") + expected = Index(2 ** 63 + np.array([10, 20], dtype="uint64")) + tm.assert_index_equal(inner, inner2) + tm.assert_index_equal(inner, expected) + + left = index_large.join(other, how="left") + tm.assert_index_equal(left, index_large.astype(object)) + + left2 = other.join(index_large, how="left") + tm.assert_index_equal(left2, other) + + right = index_large.join(other, how="right") + tm.assert_index_equal(right, other) + + right2 = other.join(index_large, how="right") + tm.assert_index_equal(right2, index_large.astype(object)) + + def test_join_outer(self, index_large): + other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) + other_mono = UInt64Index( + 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") + ) + + # not monotonic + # guarantee of sortedness + res, lidx, ridx = index_large.join(other, how="outer", return_indexers=True) + noidx_res = index_large.join(other, how="outer") + tm.assert_index_equal(res, noidx_res) + + eres = UInt64Index( + 2 ** 63 + np.array([0, 1, 2, 7, 10, 12, 15, 20, 25], dtype="uint64") + ) + elidx = np.array([0, -1, -1, -1, 1, -1, 2, 3, 4], dtype=np.intp) + eridx = np.array([-1, 3, 4, 0, 5, 1, -1, -1, 2], dtype=np.intp) + + assert isinstance(res, UInt64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) + + # monotonic + res, lidx, ridx = index_large.join( + other_mono, how="outer", return_indexers=True + ) + noidx_res = index_large.join(other_mono, how="outer") + tm.assert_index_equal(res, noidx_res) + + elidx = np.array([0, -1, -1, -1, 1, -1, 2, 3, 4], dtype=np.intp) + eridx = np.array([-1, 0, 1, 2, 3, 4, -1, -1, 5], dtype=np.intp) + + assert isinstance(res, UInt64Index) + tm.assert_index_equal(res, eres) + tm.assert_numpy_array_equal(lidx, elidx) + tm.assert_numpy_array_equal(ridx, eridx) diff --git a/pandas/tests/indexes/period/test_asfreq.py b/pandas/tests/indexes/period/test_asfreq.py index 88e800d66f3ad..8c04ac1177676 100644 --- a/pandas/tests/indexes/period/test_asfreq.py +++ b/pandas/tests/indexes/period/test_asfreq.py @@ -1,8 +1,6 @@ -import numpy as np import pytest -import pandas as pd -from pandas import DataFrame, PeriodIndex, Series, period_range +from pandas import PeriodIndex, period_range import pandas._testing as tm @@ -98,7 +96,7 @@ def test_asfreq_mult_pi(self, freq): assert result.freq == exp.freq def test_asfreq_combined_pi(self): - pi = pd.PeriodIndex(["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq="H") + pi = PeriodIndex(["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq="H") exp = PeriodIndex(["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq="25H") for freq, how in zip(["1D1H", "1H1D"], ["S", "E"]): result = pi.asfreq(freq, how=how) @@ -106,38 +104,18 @@ def test_asfreq_combined_pi(self): assert result.freq == exp.freq for freq in ["1D1H", "1H1D"]: - pi = pd.PeriodIndex( - ["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq=freq - ) + pi = PeriodIndex(["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq=freq) result = pi.asfreq("H") exp = PeriodIndex(["2001-01-02 00:00", "2001-01-03 02:00", "NaT"], freq="H") tm.assert_index_equal(result, exp) assert result.freq == exp.freq - pi = pd.PeriodIndex( - ["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq=freq - ) + pi = PeriodIndex(["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq=freq) result = pi.asfreq("H", how="S") exp = PeriodIndex(["2001-01-01 00:00", "2001-01-02 02:00", "NaT"], freq="H") tm.assert_index_equal(result, exp) assert result.freq == exp.freq - def test_asfreq_ts(self): - index = period_range(freq="A", start="1/1/2001", end="12/31/2010") - ts = Series(np.random.randn(len(index)), index=index) - df = DataFrame(np.random.randn(len(index), 3), index=index) - - result = ts.asfreq("D", how="end") - df_result = df.asfreq("D", how="end") - exp_index = index.asfreq("D", how="end") - assert len(result) == len(ts) - tm.assert_index_equal(result.index, exp_index) - tm.assert_index_equal(df_result.index, exp_index) - - result = ts.asfreq("D", how="start") - assert len(result) == len(ts) - tm.assert_index_equal(result.index, index.asfreq("D", how="start")) - def test_astype_asfreq(self): pi1 = PeriodIndex(["2011-01-01", "2011-02-01", "2011-03-01"], freq="D") exp = PeriodIndex(["2011-01", "2011-02", "2011-03"], freq="M") diff --git a/pandas/tests/indexes/period/test_astype.py b/pandas/tests/indexes/period/test_astype.py index ec386dd9dd11c..2f10e45193d5d 100644 --- a/pandas/tests/indexes/period/test_astype.py +++ b/pandas/tests/indexes/period/test_astype.py @@ -1,8 +1,18 @@ import numpy as np import pytest -import pandas as pd -from pandas import Index, Int64Index, NaT, Period, PeriodIndex, period_range +from pandas import ( + CategoricalIndex, + DatetimeIndex, + Index, + Int64Index, + NaT, + Period, + PeriodIndex, + Timedelta, + UInt64Index, + period_range, +) import pandas._testing as tm @@ -41,39 +51,39 @@ def test_astype_conversion(self): def test_astype_uint(self): arr = period_range("2000", periods=2) - expected = pd.UInt64Index(np.array([10957, 10958], dtype="uint64")) + expected = UInt64Index(np.array([10957, 10958], dtype="uint64")) tm.assert_index_equal(arr.astype("uint64"), expected) tm.assert_index_equal(arr.astype("uint32"), expected) def test_astype_object(self): - idx = pd.PeriodIndex([], freq="M") + idx = PeriodIndex([], freq="M") exp = np.array([], dtype=object) tm.assert_numpy_array_equal(idx.astype(object).values, exp) tm.assert_numpy_array_equal(idx._mpl_repr(), exp) - idx = pd.PeriodIndex(["2011-01", pd.NaT], freq="M") + idx = PeriodIndex(["2011-01", NaT], freq="M") - exp = np.array([pd.Period("2011-01", freq="M"), pd.NaT], dtype=object) + exp = np.array([Period("2011-01", freq="M"), NaT], dtype=object) tm.assert_numpy_array_equal(idx.astype(object).values, exp) tm.assert_numpy_array_equal(idx._mpl_repr(), exp) - exp = np.array([pd.Period("2011-01-01", freq="D"), pd.NaT], dtype=object) - idx = pd.PeriodIndex(["2011-01-01", pd.NaT], freq="D") + exp = np.array([Period("2011-01-01", freq="D"), NaT], dtype=object) + idx = PeriodIndex(["2011-01-01", NaT], freq="D") tm.assert_numpy_array_equal(idx.astype(object).values, exp) tm.assert_numpy_array_equal(idx._mpl_repr(), exp) # TODO: de-duplicate this version (from test_ops) with the one above # (from test_period) def test_astype_object2(self): - idx = pd.period_range(start="2013-01-01", periods=4, freq="M", name="idx") + idx = period_range(start="2013-01-01", periods=4, freq="M", name="idx") expected_list = [ - pd.Period("2013-01-31", freq="M"), - pd.Period("2013-02-28", freq="M"), - pd.Period("2013-03-31", freq="M"), - pd.Period("2013-04-30", freq="M"), + Period("2013-01-31", freq="M"), + Period("2013-02-28", freq="M"), + Period("2013-03-31", freq="M"), + Period("2013-04-30", freq="M"), ] - expected = pd.Index(expected_list, dtype=object, name="idx") + expected = Index(expected_list, dtype=object, name="idx") result = idx.astype(object) assert isinstance(result, Index) assert result.dtype == object @@ -85,31 +95,31 @@ def test_astype_object2(self): ["2013-01-01", "2013-01-02", "NaT", "2013-01-04"], freq="D", name="idx" ) expected_list = [ - pd.Period("2013-01-01", freq="D"), - pd.Period("2013-01-02", freq="D"), - pd.Period("NaT", freq="D"), - pd.Period("2013-01-04", freq="D"), + Period("2013-01-01", freq="D"), + Period("2013-01-02", freq="D"), + Period("NaT", freq="D"), + Period("2013-01-04", freq="D"), ] - expected = pd.Index(expected_list, dtype=object, name="idx") + expected = Index(expected_list, dtype=object, name="idx") result = idx.astype(object) assert isinstance(result, Index) assert result.dtype == object tm.assert_index_equal(result, expected) for i in [0, 1, 3]: assert result[i] == expected[i] - assert result[2] is pd.NaT + assert result[2] is NaT assert result.name == expected.name result_list = idx.tolist() for i in [0, 1, 3]: assert result_list[i] == expected_list[i] - assert result_list[2] is pd.NaT + assert result_list[2] is NaT def test_astype_category(self): - obj = pd.period_range("2000", periods=2) + obj = period_range("2000", periods=2) result = obj.astype("category") - expected = pd.CategoricalIndex( - [pd.Period("2000-01-01", freq="D"), pd.Period("2000-01-02", freq="D")] + expected = CategoricalIndex( + [Period("2000-01-01", freq="D"), Period("2000-01-02", freq="D")] ) tm.assert_index_equal(result, expected) @@ -118,11 +128,30 @@ def test_astype_category(self): tm.assert_categorical_equal(result, expected) def test_astype_array_fallback(self): - obj = pd.period_range("2000", periods=2) + obj = period_range("2000", periods=2) result = obj.astype(bool) - expected = pd.Index(np.array([True, True])) + expected = Index(np.array([True, True])) tm.assert_index_equal(result, expected) result = obj._data.astype(bool) expected = np.array([True, True]) tm.assert_numpy_array_equal(result, expected) + + def test_period_astype_to_timestamp(self): + pi = PeriodIndex(["2011-01", "2011-02", "2011-03"], freq="M") + + exp = DatetimeIndex(["2011-01-01", "2011-02-01", "2011-03-01"]) + tm.assert_index_equal(pi.astype("datetime64[ns]"), exp) + + exp = DatetimeIndex(["2011-01-31", "2011-02-28", "2011-03-31"]) + exp = exp + Timedelta(1, "D") - Timedelta(1, "ns") + tm.assert_index_equal(pi.astype("datetime64[ns]", how="end"), exp) + + exp = DatetimeIndex(["2011-01-01", "2011-02-01", "2011-03-01"], tz="US/Eastern") + res = pi.astype("datetime64[ns, US/Eastern]") + tm.assert_index_equal(pi.astype("datetime64[ns, US/Eastern]"), exp) + + exp = DatetimeIndex(["2011-01-31", "2011-02-28", "2011-03-31"], tz="US/Eastern") + exp = exp + Timedelta(1, "D") - Timedelta(1, "ns") + res = pi.astype("datetime64[ns, US/Eastern]", how="end") + tm.assert_index_equal(res, exp) diff --git a/pandas/tests/indexes/period/test_constructors.py b/pandas/tests/indexes/period/test_constructors.py index dcd3c8e946e9a..b5ff83ec7514d 100644 --- a/pandas/tests/indexes/period/test_constructors.py +++ b/pandas/tests/indexes/period/test_constructors.py @@ -6,7 +6,16 @@ from pandas.core.dtypes.dtypes import PeriodDtype import pandas as pd -from pandas import Index, Period, PeriodIndex, Series, date_range, offsets, period_range +from pandas import ( + Index, + NaT, + Period, + PeriodIndex, + Series, + date_range, + offsets, + period_range, +) import pandas._testing as tm from pandas.core.arrays import PeriodArray @@ -14,27 +23,25 @@ class TestPeriodIndex: def test_construction_base_constructor(self): # GH 13664 - arr = [pd.Period("2011-01", freq="M"), pd.NaT, pd.Period("2011-03", freq="M")] - tm.assert_index_equal(pd.Index(arr), pd.PeriodIndex(arr)) - tm.assert_index_equal(pd.Index(np.array(arr)), pd.PeriodIndex(np.array(arr))) + arr = [Period("2011-01", freq="M"), NaT, Period("2011-03", freq="M")] + tm.assert_index_equal(Index(arr), PeriodIndex(arr)) + tm.assert_index_equal(Index(np.array(arr)), PeriodIndex(np.array(arr))) - arr = [np.nan, pd.NaT, pd.Period("2011-03", freq="M")] - tm.assert_index_equal(pd.Index(arr), pd.PeriodIndex(arr)) - tm.assert_index_equal(pd.Index(np.array(arr)), pd.PeriodIndex(np.array(arr))) + arr = [np.nan, NaT, Period("2011-03", freq="M")] + tm.assert_index_equal(Index(arr), PeriodIndex(arr)) + tm.assert_index_equal(Index(np.array(arr)), PeriodIndex(np.array(arr))) - arr = [pd.Period("2011-01", freq="M"), pd.NaT, pd.Period("2011-03", freq="D")] - tm.assert_index_equal(pd.Index(arr), pd.Index(arr, dtype=object)) + arr = [Period("2011-01", freq="M"), NaT, Period("2011-03", freq="D")] + tm.assert_index_equal(Index(arr), Index(arr, dtype=object)) - tm.assert_index_equal( - pd.Index(np.array(arr)), pd.Index(np.array(arr), dtype=object) - ) + tm.assert_index_equal(Index(np.array(arr)), Index(np.array(arr), dtype=object)) def test_base_constructor_with_period_dtype(self): dtype = PeriodDtype("D") values = ["2011-01-01", "2012-03-04", "2014-05-01"] - result = pd.Index(values, dtype=dtype) + result = Index(values, dtype=dtype) - expected = pd.PeriodIndex(values, dtype=dtype) + expected = PeriodIndex(values, dtype=dtype) tm.assert_index_equal(result, expected) @pytest.mark.parametrize( @@ -43,9 +50,9 @@ def test_base_constructor_with_period_dtype(self): def test_index_object_dtype(self, values_constructor): # Index(periods, dtype=object) is an Index (not an PeriodIndex) periods = [ - pd.Period("2011-01", freq="M"), - pd.NaT, - pd.Period("2011-03", freq="M"), + Period("2011-01", freq="M"), + NaT, + Period("2011-03", freq="M"), ] values = values_constructor(periods) result = Index(values, dtype=object) @@ -118,8 +125,8 @@ def test_constructor_arrays_negative_year(self): pindex = PeriodIndex(year=years, quarter=quarters) - tm.assert_index_equal(pindex.year, pd.Index(years)) - tm.assert_index_equal(pindex.quarter, pd.Index(quarters)) + tm.assert_index_equal(pindex.year, Index(years)) + tm.assert_index_equal(pindex.quarter, Index(quarters)) def test_constructor_invalid_quarters(self): msg = "Quarter must be 1 <= q <= 4" @@ -184,7 +191,7 @@ def test_constructor_datetime64arr(self): @pytest.mark.parametrize("box", [None, "series", "index"]) def test_constructor_datetime64arr_ok(self, box): # https://github.com/pandas-dev/pandas/issues/23438 - data = pd.date_range("2017", periods=4, freq="M") + data = date_range("2017", periods=4, freq="M") if box is None: data = data._values elif box == "series": @@ -226,52 +233,47 @@ def test_constructor_dtype(self): PeriodIndex(["2011-01"], freq="M", dtype="period[D]") def test_constructor_empty(self): - idx = pd.PeriodIndex([], freq="M") + idx = PeriodIndex([], freq="M") assert isinstance(idx, PeriodIndex) assert len(idx) == 0 assert idx.freq == "M" with pytest.raises(ValueError, match="freq not specified"): - pd.PeriodIndex([]) + PeriodIndex([]) def test_constructor_pi_nat(self): idx = PeriodIndex( - [Period("2011-01", freq="M"), pd.NaT, Period("2011-01", freq="M")] + [Period("2011-01", freq="M"), NaT, Period("2011-01", freq="M")] ) exp = PeriodIndex(["2011-01", "NaT", "2011-01"], freq="M") tm.assert_index_equal(idx, exp) idx = PeriodIndex( - np.array([Period("2011-01", freq="M"), pd.NaT, Period("2011-01", freq="M")]) + np.array([Period("2011-01", freq="M"), NaT, Period("2011-01", freq="M")]) ) tm.assert_index_equal(idx, exp) idx = PeriodIndex( - [pd.NaT, pd.NaT, Period("2011-01", freq="M"), Period("2011-01", freq="M")] + [NaT, NaT, Period("2011-01", freq="M"), Period("2011-01", freq="M")] ) exp = PeriodIndex(["NaT", "NaT", "2011-01", "2011-01"], freq="M") tm.assert_index_equal(idx, exp) idx = PeriodIndex( np.array( - [ - pd.NaT, - pd.NaT, - Period("2011-01", freq="M"), - Period("2011-01", freq="M"), - ] + [NaT, NaT, Period("2011-01", freq="M"), Period("2011-01", freq="M")] ) ) tm.assert_index_equal(idx, exp) - idx = PeriodIndex([pd.NaT, pd.NaT, "2011-01", "2011-01"], freq="M") + idx = PeriodIndex([NaT, NaT, "2011-01", "2011-01"], freq="M") tm.assert_index_equal(idx, exp) with pytest.raises(ValueError, match="freq not specified"): - PeriodIndex([pd.NaT, pd.NaT]) + PeriodIndex([NaT, NaT]) with pytest.raises(ValueError, match="freq not specified"): - PeriodIndex(np.array([pd.NaT, pd.NaT])) + PeriodIndex(np.array([NaT, NaT])) with pytest.raises(ValueError, match="freq not specified"): PeriodIndex(["NaT", "NaT"]) @@ -283,40 +285,36 @@ def test_constructor_incompat_freq(self): msg = "Input has different freq=D from PeriodIndex\\(freq=M\\)" with pytest.raises(IncompatibleFrequency, match=msg): - PeriodIndex( - [Period("2011-01", freq="M"), pd.NaT, Period("2011-01", freq="D")] - ) + PeriodIndex([Period("2011-01", freq="M"), NaT, Period("2011-01", freq="D")]) with pytest.raises(IncompatibleFrequency, match=msg): PeriodIndex( np.array( - [Period("2011-01", freq="M"), pd.NaT, Period("2011-01", freq="D")] + [Period("2011-01", freq="M"), NaT, Period("2011-01", freq="D")] ) ) - # first element is pd.NaT + # first element is NaT with pytest.raises(IncompatibleFrequency, match=msg): - PeriodIndex( - [pd.NaT, Period("2011-01", freq="M"), Period("2011-01", freq="D")] - ) + PeriodIndex([NaT, Period("2011-01", freq="M"), Period("2011-01", freq="D")]) with pytest.raises(IncompatibleFrequency, match=msg): PeriodIndex( np.array( - [pd.NaT, Period("2011-01", freq="M"), Period("2011-01", freq="D")] + [NaT, Period("2011-01", freq="M"), Period("2011-01", freq="D")] ) ) def test_constructor_mixed(self): - idx = PeriodIndex(["2011-01", pd.NaT, Period("2011-01", freq="M")]) + idx = PeriodIndex(["2011-01", NaT, Period("2011-01", freq="M")]) exp = PeriodIndex(["2011-01", "NaT", "2011-01"], freq="M") tm.assert_index_equal(idx, exp) - idx = PeriodIndex(["NaT", pd.NaT, Period("2011-01", freq="M")]) + idx = PeriodIndex(["NaT", NaT, Period("2011-01", freq="M")]) exp = PeriodIndex(["NaT", "NaT", "2011-01"], freq="M") tm.assert_index_equal(idx, exp) - idx = PeriodIndex([Period("2011-01-01", freq="D"), pd.NaT, "2012-01-01"]) + idx = PeriodIndex([Period("2011-01-01", freq="D"), NaT, "2012-01-01"]) exp = PeriodIndex(["2011-01-01", "NaT", "2012-01-01"], freq="D") tm.assert_index_equal(idx, exp) @@ -324,9 +322,9 @@ def test_constructor_simple_new(self): idx = period_range("2007-01", name="p", periods=2, freq="M") with pytest.raises(AssertionError, match=""): - idx._simple_new(idx, name="p", freq=idx.freq) + idx._simple_new(idx, name="p") - result = idx._simple_new(idx._data, name="p", freq=idx.freq) + result = idx._simple_new(idx._data, name="p") tm.assert_index_equal(result, idx) with pytest.raises(AssertionError): @@ -341,19 +339,19 @@ def test_constructor_simple_new_empty(self): # GH13079 idx = PeriodIndex([], freq="M", name="p") with pytest.raises(AssertionError, match=""): - idx._simple_new(idx, name="p", freq="M") + idx._simple_new(idx, name="p") - result = idx._simple_new(idx._data, name="p", freq="M") + result = idx._simple_new(idx._data, name="p") tm.assert_index_equal(result, idx) @pytest.mark.parametrize("floats", [[1.1, 2.1], np.array([1.1, 2.1])]) def test_constructor_floats(self, floats): with pytest.raises(AssertionError, match=" np.ndarray: result = pd.Index(ArrayLike(array)) tm.assert_index_equal(result, expected) - @pytest.mark.parametrize( - "dtype", - [int, "int64", "int32", "int16", "int8", "uint64", "uint32", "uint16", "uint8"], - ) - def test_constructor_int_dtype_float(self, dtype): - # GH 18400 - if is_unsigned_integer_dtype(dtype): - index_type = UInt64Index - else: - index_type = Int64Index - - expected = index_type([0, 1, 2, 3]) - result = Index([0.0, 1.0, 2.0, 3.0], dtype=dtype) - tm.assert_index_equal(result, expected) - def test_constructor_int_dtype_nan(self): # see gh-15187 data = [np.nan] @@ -305,6 +280,10 @@ def test_index_ctor_infer_nat_dt_like(self, pos, klass, dtype, ctor, nulls_fixtu data = [ctor] data.insert(pos, nulls_fixture) + if nulls_fixture is pd.NA: + expected = Index([pd.NA, pd.NaT]) + pytest.xfail("Broken with np.NaT ctor; see GH 31884") + result = Index(data) tm.assert_index_equal(result, expected) @@ -370,19 +349,6 @@ def test_constructor_dtypes_to_float64(self, vals): index = Index(vals, dtype=float) assert isinstance(index, Float64Index) - @pytest.mark.parametrize("cast_index", [True, False]) - @pytest.mark.parametrize( - "vals", [[True, False, True], np.array([True, False, True], dtype=bool)] - ) - def test_constructor_dtypes_to_object(self, cast_index, vals): - if cast_index: - index = Index(vals, dtype=bool) - else: - index = Index(vals) - - assert isinstance(index, Index) - assert index.dtype == object - @pytest.mark.parametrize( "vals", [ @@ -587,25 +553,6 @@ def test_equals_object(self): def test_not_equals_object(self, comp): assert not Index(["a", "b", "c"]).equals(comp) - def test_insert(self): - - # GH 7256 - # validate neg/pos inserts - result = Index(["b", "c", "d"]) - - # test 0th element - tm.assert_index_equal(Index(["a", "b", "c", "d"]), result.insert(0, "a")) - - # test Nth element that follows Python list behavior - tm.assert_index_equal(Index(["b", "c", "e", "d"]), result.insert(-1, "e")) - - # test loc +/- neq (0, -1) - tm.assert_index_equal(result.insert(1, "z"), result.insert(-2, "z")) - - # test empty - null_index = Index([]) - tm.assert_index_equal(Index(["a"]), null_index.insert(0, "a")) - def test_insert_missing(self, nulls_fixture): # GH 22295 # test there is no mangling of NA values @@ -613,19 +560,6 @@ def test_insert_missing(self, nulls_fixture): result = Index(list("abc")).insert(1, nulls_fixture) tm.assert_index_equal(result, expected) - @pytest.mark.parametrize( - "pos,expected", - [ - (0, Index(["b", "c", "d"], name="index")), - (-1, Index(["a", "b", "c"], name="index")), - ], - ) - def test_delete(self, pos, expected): - index = Index(["a", "b", "c", "d"], name="index") - result = index.delete(pos) - tm.assert_index_equal(result, expected) - assert result.name == expected.name - def test_delete_raises(self): index = Index(["a", "b", "c", "d"], name="index") msg = "index 5 is out of bounds for axis 0 with size 4" @@ -839,16 +773,6 @@ def test_intersect_str_dates(self, sort): assert len(result) == 0 - def test_intersect_nosort(self): - result = pd.Index(["c", "b", "a"]).intersection(["b", "a"]) - expected = pd.Index(["b", "a"]) - tm.assert_index_equal(result, expected) - - def test_intersection_equal_sort(self): - idx = pd.Index(["c", "a", "b"]) - tm.assert_index_equal(idx.intersection(idx, sort=False), idx) - tm.assert_index_equal(idx.intersection(idx, sort=None), idx) - @pytest.mark.xfail(reason="Not implemented") def test_intersection_equal_sort_true(self): # TODO decide on True behaviour @@ -910,32 +834,6 @@ def test_union_sort_special_true(self, slice_): expected = pd.Index([0, 1, 2]) tm.assert_index_equal(result, expected) - def test_union_sort_other_incomparable(self): - # https://github.com/pandas-dev/pandas/issues/24959 - idx = pd.Index([1, pd.Timestamp("2000")]) - # default (sort=None) - with tm.assert_produces_warning(RuntimeWarning): - result = idx.union(idx[:1]) - - tm.assert_index_equal(result, idx) - - # sort=None - with tm.assert_produces_warning(RuntimeWarning): - result = idx.union(idx[:1], sort=None) - tm.assert_index_equal(result, idx) - - # sort=False - result = idx.union(idx[:1], sort=False) - tm.assert_index_equal(result, idx) - - @pytest.mark.xfail(reason="Not implemented") - def test_union_sort_other_incomparable_true(self): - # TODO decide on True behaviour - # sort=True - idx = pd.Index([1, pd.Timestamp("2000")]) - with pytest.raises(TypeError, match=".*"): - idx.union(idx[:1], sort=True) - @pytest.mark.parametrize("klass", [np.array, Series, list]) @pytest.mark.parametrize("sort", [None, False]) def test_union_from_iterables(self, index, klass, sort): @@ -1008,42 +906,6 @@ def test_union_dt_as_obj(self, sort): tm.assert_contains_all(index, second_cat) tm.assert_contains_all(date_index, first_cat) - @pytest.mark.parametrize( - "method", ["union", "intersection", "difference", "symmetric_difference"] - ) - def test_setops_disallow_true(self, method): - idx1 = pd.Index(["a", "b"]) - idx2 = pd.Index(["b", "c"]) - - with pytest.raises(ValueError, match="The 'sort' keyword only takes"): - getattr(idx1, method)(idx2, sort=True) - - def test_setops_preserve_object_dtype(self): - idx = pd.Index([1, 2, 3], dtype=object) - result = idx.intersection(idx[1:]) - expected = idx[1:] - tm.assert_index_equal(result, expected) - - # if other is not monotonic increasing, intersection goes through - # a different route - result = idx.intersection(idx[1:][::-1]) - tm.assert_index_equal(result, expected) - - result = idx._union(idx[1:], sort=None) - expected = idx - tm.assert_index_equal(result, expected) - - result = idx.union(idx[1:], sort=None) - tm.assert_index_equal(result, expected) - - # if other is not monotonic increasing, _union goes through - # a different route - result = idx._union(idx[1:][::-1], sort=None) - tm.assert_index_equal(result, expected) - - result = idx.union(idx[1:][::-1], sort=None) - tm.assert_index_equal(result, expected) - def test_map_identity_mapping(self, indices): # GH 12766 tm.assert_index_equal(indices, indices.map(lambda x: x)) @@ -1151,17 +1013,6 @@ def test_map_defaultdict(self): expected = Index(["stuff", "blank", "blank"]) tm.assert_index_equal(result, expected) - def test_append_multiple(self): - index = Index(["a", "b", "c", "d", "e", "f"]) - - foos = [index[:2], index[2:4], index[4:]] - result = foos[0].append(foos[1:]) - tm.assert_index_equal(result, index) - - # empty - result = index.append([]) - tm.assert_index_equal(result, index) - @pytest.mark.parametrize("name,expected", [("foo", "foo"), ("bar", None)]) def test_append_empty_preserve_name(self, name, expected): left = Index([], name="foo") @@ -1964,6 +1815,9 @@ def test_isin_nan_common_float64(self, nulls_fixture): pytest.skip("pd.NaT not compatible with Float64Index") # Float64Index overrides isin, so must be checked separately + if nulls_fixture is pd.NA: + pytest.xfail("Float64Index cannot contain pd.NA") + tm.assert_numpy_array_equal( Float64Index([1.0, nulls_fixture]).isin([np.nan]), np.array([False, True]) ) @@ -2049,7 +1903,9 @@ def test_slice_keep_name(self): assert index.name == index[1:].name @pytest.mark.parametrize( - "index", ["unicode", "string", "datetime", "int", "float"], indirect=True + "index", + ["unicode", "string", "datetime", "int", "uint", "float"], + indirect=True, ) def test_join_self(self, index, join_type): joined = index.join(index, how=join_type) @@ -2437,7 +2293,6 @@ class TestMixedIntIndex(Base): # Mostly the tests from common.py for which the results differ # in py2 and py3 because ints and strings are uncomparable in py3 # (GH 13514) - _holder = Index @pytest.fixture(params=[[0, "a", 1, "b", 2, "c"]], ids=["mixedIndex"]) @@ -2573,14 +2428,6 @@ def test_get_combined_index(self): expected = Index([]) tm.assert_index_equal(result, expected) - def test_repeat(self): - repeats = 2 - index = pd.Index([1, 2, 3]) - expected = pd.Index([1, 1, 2, 2, 3, 3]) - - result = index.repeat(repeats) - tm.assert_index_equal(result, expected) - @pytest.mark.parametrize( "index", [ @@ -2764,3 +2611,18 @@ def test_validate_1d_input(): ser = pd.Series(0, range(4)) with pytest.raises(ValueError, match=msg): ser.index = np.array([[2, 3]] * 4) + + +def test_convert_almost_null_slice(indices): + # slice with None at both ends, but not step + idx = indices + + key = slice(None, None, "foo") + + if isinstance(idx, pd.IntervalIndex): + with pytest.raises(ValueError, match="cannot support not-default step"): + idx._convert_slice_indexer(key, "loc") + else: + msg = "'>=' not supported between instances of 'str' and 'int'" + with pytest.raises(TypeError, match=msg): + idx._convert_slice_indexer(key, "loc") diff --git a/pandas/tests/indexes/test_common.py b/pandas/tests/indexes/test_common.py index 7e30233353553..b46e6514b4536 100644 --- a/pandas/tests/indexes/test_common.py +++ b/pandas/tests/indexes/test_common.py @@ -158,13 +158,6 @@ def test_set_name_methods(self, indices): assert indices.name == name assert indices.names == [name] - def test_hash_error(self, indices): - index = indices - with pytest.raises( - TypeError, match=f"unhashable type: '{type(index).__name__}'" - ): - hash(indices) - def test_copy_and_deepcopy(self, indices): from copy import copy, deepcopy @@ -246,11 +239,6 @@ def test_get_unique_index(self, indices): result = i._get_unique_index(dropna=dropna) tm.assert_index_equal(result, expected) - def test_sort(self, indices): - msg = "cannot sort an Index object in-place, use sort_values instead" - with pytest.raises(TypeError, match=msg): - indices.sort() - def test_mutability(self, indices): if not len(indices): pytest.skip("Skip check for empty Index") @@ -261,9 +249,6 @@ def test_mutability(self, indices): def test_view(self, indices): assert indices.view().name == indices.name - def test_compat(self, indices): - assert indices.tolist() == list(indices) - def test_searchsorted_monotonic(self, indices): # GH17271 # not implemented for tuple searches in MultiIndex diff --git a/pandas/tests/indexes/test_index_new.py b/pandas/tests/indexes/test_index_new.py new file mode 100644 index 0000000000000..33f61de6a4ebf --- /dev/null +++ b/pandas/tests/indexes/test_index_new.py @@ -0,0 +1,55 @@ +""" +Tests for the Index constructor conducting inference. +""" +import numpy as np +import pytest + +from pandas.core.dtypes.common import is_unsigned_integer_dtype + +from pandas import CategoricalIndex, Index, Int64Index, MultiIndex, UInt64Index +import pandas._testing as tm + + +class TestIndexConstructorInference: + @pytest.mark.parametrize("na_value", [None, np.nan]) + @pytest.mark.parametrize("vtype", [list, tuple, iter]) + def test_construction_list_tuples_nan(self, na_value, vtype): + # GH#18505 : valid tuples containing NaN + values = [(1, "two"), (3.0, na_value)] + result = Index(vtype(values)) + expected = MultiIndex.from_tuples(values) + tm.assert_index_equal(result, expected) + + @pytest.mark.parametrize( + "dtype", + [int, "int64", "int32", "int16", "int8", "uint64", "uint32", "uint16", "uint8"], + ) + def test_constructor_int_dtype_float(self, dtype): + # GH#18400 + if is_unsigned_integer_dtype(dtype): + index_type = UInt64Index + else: + index_type = Int64Index + + expected = index_type([0, 1, 2, 3]) + result = Index([0.0, 1.0, 2.0, 3.0], dtype=dtype) + tm.assert_index_equal(result, expected) + + @pytest.mark.parametrize("cast_index", [True, False]) + @pytest.mark.parametrize( + "vals", [[True, False, True], np.array([True, False, True], dtype=bool)] + ) + def test_constructor_dtypes_to_object(self, cast_index, vals): + if cast_index: + index = Index(vals, dtype=bool) + else: + index = Index(vals) + + assert type(index) is Index + assert index.dtype == object + + def test_constructor_categorical_to_object(self): + # GH#32167 Categorical data and dtype=object should return object-dtype + ci = CategoricalIndex(range(5)) + result = Index(ci, dtype=object) + assert not isinstance(result, CategoricalIndex) diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py index 1b504ce99604d..10d57d8616cf3 100644 --- a/pandas/tests/indexes/test_numeric.py +++ b/pandas/tests/indexes/test_numeric.py @@ -580,25 +580,6 @@ def test_identical(self): assert not index.copy(dtype=object).identical(index.copy(dtype=self._dtype)) - def test_join_non_unique(self): - left = Index([4, 4, 3, 3]) - - joined, lidx, ridx = left.join(left, return_indexers=True) - - exp_joined = Index([3, 3, 3, 3, 4, 4, 4, 4]) - tm.assert_index_equal(joined, exp_joined) - - exp_lidx = np.array([2, 2, 3, 3, 0, 0, 1, 1], dtype=np.intp) - tm.assert_numpy_array_equal(lidx, exp_lidx) - - exp_ridx = np.array([2, 3, 2, 3, 0, 1, 0, 1], dtype=np.intp) - tm.assert_numpy_array_equal(ridx, exp_ridx) - - def test_join_self(self, join_type): - index = self.create_index() - joined = index.join(index, how=join_type) - assert index is joined - def test_union_noncomparable(self): # corner case, non-Int64Index index = self.create_index() @@ -798,175 +779,6 @@ def test_intersection(self): ) tm.assert_index_equal(result, expected) - def test_join_inner(self): - index = self.create_index() - other = Int64Index([7, 12, 25, 1, 2, 5]) - other_mono = Int64Index([1, 2, 5, 7, 12, 25]) - - # not monotonic - res, lidx, ridx = index.join(other, how="inner", return_indexers=True) - - # no guarantee of sortedness, so sort for comparison purposes - ind = res.argsort() - res = res.take(ind) - lidx = lidx.take(ind) - ridx = ridx.take(ind) - - eres = Int64Index([2, 12]) - elidx = np.array([1, 6], dtype=np.intp) - eridx = np.array([4, 1], dtype=np.intp) - - assert isinstance(res, Int64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - # monotonic - res, lidx, ridx = index.join(other_mono, how="inner", return_indexers=True) - - res2 = index.intersection(other_mono) - tm.assert_index_equal(res, res2) - - elidx = np.array([1, 6], dtype=np.intp) - eridx = np.array([1, 4], dtype=np.intp) - assert isinstance(res, Int64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - def test_join_left(self): - index = self.create_index() - other = Int64Index([7, 12, 25, 1, 2, 5]) - other_mono = Int64Index([1, 2, 5, 7, 12, 25]) - - # not monotonic - res, lidx, ridx = index.join(other, how="left", return_indexers=True) - eres = index - eridx = np.array([-1, 4, -1, -1, -1, -1, 1, -1, -1, -1], dtype=np.intp) - - assert isinstance(res, Int64Index) - tm.assert_index_equal(res, eres) - assert lidx is None - tm.assert_numpy_array_equal(ridx, eridx) - - # monotonic - res, lidx, ridx = index.join(other_mono, how="left", return_indexers=True) - eridx = np.array([-1, 1, -1, -1, -1, -1, 4, -1, -1, -1], dtype=np.intp) - assert isinstance(res, Int64Index) - tm.assert_index_equal(res, eres) - assert lidx is None - tm.assert_numpy_array_equal(ridx, eridx) - - # non-unique - idx = Index([1, 1, 2, 5]) - idx2 = Index([1, 2, 5, 7, 9]) - res, lidx, ridx = idx2.join(idx, how="left", return_indexers=True) - eres = Index([1, 1, 2, 5, 7, 9]) # 1 is in idx2, so it should be x2 - eridx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) - elidx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - def test_join_right(self): - index = self.create_index() - other = Int64Index([7, 12, 25, 1, 2, 5]) - other_mono = Int64Index([1, 2, 5, 7, 12, 25]) - - # not monotonic - res, lidx, ridx = index.join(other, how="right", return_indexers=True) - eres = other - elidx = np.array([-1, 6, -1, -1, 1, -1], dtype=np.intp) - - assert isinstance(other, Int64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - assert ridx is None - - # monotonic - res, lidx, ridx = index.join(other_mono, how="right", return_indexers=True) - eres = other_mono - elidx = np.array([-1, 1, -1, -1, 6, -1], dtype=np.intp) - assert isinstance(other, Int64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - assert ridx is None - - # non-unique - idx = Index([1, 1, 2, 5]) - idx2 = Index([1, 2, 5, 7, 9]) - res, lidx, ridx = idx.join(idx2, how="right", return_indexers=True) - eres = Index([1, 1, 2, 5, 7, 9]) # 1 is in idx2, so it should be x2 - elidx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) - eridx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - def test_join_non_int_index(self): - index = self.create_index() - other = Index([3, 6, 7, 8, 10], dtype=object) - - outer = index.join(other, how="outer") - outer2 = other.join(index, how="outer") - expected = Index([0, 2, 3, 4, 6, 7, 8, 10, 12, 14, 16, 18]) - tm.assert_index_equal(outer, outer2) - tm.assert_index_equal(outer, expected) - - inner = index.join(other, how="inner") - inner2 = other.join(index, how="inner") - expected = Index([6, 8, 10]) - tm.assert_index_equal(inner, inner2) - tm.assert_index_equal(inner, expected) - - left = index.join(other, how="left") - tm.assert_index_equal(left, index.astype(object)) - - left2 = other.join(index, how="left") - tm.assert_index_equal(left2, other) - - right = index.join(other, how="right") - tm.assert_index_equal(right, other) - - right2 = other.join(index, how="right") - tm.assert_index_equal(right2, index.astype(object)) - - def test_join_outer(self): - index = self.create_index() - other = Int64Index([7, 12, 25, 1, 2, 5]) - other_mono = Int64Index([1, 2, 5, 7, 12, 25]) - - # not monotonic - # guarantee of sortedness - res, lidx, ridx = index.join(other, how="outer", return_indexers=True) - noidx_res = index.join(other, how="outer") - tm.assert_index_equal(res, noidx_res) - - eres = Int64Index([0, 1, 2, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 25]) - elidx = np.array([0, -1, 1, 2, -1, 3, -1, 4, 5, 6, 7, 8, 9, -1], dtype=np.intp) - eridx = np.array( - [-1, 3, 4, -1, 5, -1, 0, -1, -1, 1, -1, -1, -1, 2], dtype=np.intp - ) - - assert isinstance(res, Int64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - # monotonic - res, lidx, ridx = index.join(other_mono, how="outer", return_indexers=True) - noidx_res = index.join(other_mono, how="outer") - tm.assert_index_equal(res, noidx_res) - - elidx = np.array([0, -1, 1, 2, -1, 3, -1, 4, 5, 6, 7, 8, 9, -1], dtype=np.intp) - eridx = np.array( - [-1, 0, 1, -1, 2, -1, 3, -1, -1, 4, -1, -1, -1, 5], dtype=np.intp - ) - assert isinstance(res, Int64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - class TestUInt64Index(NumericInt): @@ -1043,196 +855,6 @@ def test_intersection(self, index_large): ) tm.assert_index_equal(result, expected) - def test_join_inner(self, index_large): - other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) - other_mono = UInt64Index( - 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") - ) - - # not monotonic - res, lidx, ridx = index_large.join(other, how="inner", return_indexers=True) - - # no guarantee of sortedness, so sort for comparison purposes - ind = res.argsort() - res = res.take(ind) - lidx = lidx.take(ind) - ridx = ridx.take(ind) - - eres = UInt64Index(2 ** 63 + np.array([10, 25], dtype="uint64")) - elidx = np.array([1, 4], dtype=np.intp) - eridx = np.array([5, 2], dtype=np.intp) - - assert isinstance(res, UInt64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - # monotonic - res, lidx, ridx = index_large.join( - other_mono, how="inner", return_indexers=True - ) - - res2 = index_large.intersection(other_mono) - tm.assert_index_equal(res, res2) - - elidx = np.array([1, 4], dtype=np.intp) - eridx = np.array([3, 5], dtype=np.intp) - - assert isinstance(res, UInt64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - def test_join_left(self, index_large): - other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) - other_mono = UInt64Index( - 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") - ) - - # not monotonic - res, lidx, ridx = index_large.join(other, how="left", return_indexers=True) - eres = index_large - eridx = np.array([-1, 5, -1, -1, 2], dtype=np.intp) - - assert isinstance(res, UInt64Index) - tm.assert_index_equal(res, eres) - assert lidx is None - tm.assert_numpy_array_equal(ridx, eridx) - - # monotonic - res, lidx, ridx = index_large.join(other_mono, how="left", return_indexers=True) - eridx = np.array([-1, 3, -1, -1, 5], dtype=np.intp) - - assert isinstance(res, UInt64Index) - tm.assert_index_equal(res, eres) - assert lidx is None - tm.assert_numpy_array_equal(ridx, eridx) - - # non-unique - idx = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5], dtype="uint64")) - idx2 = UInt64Index(2 ** 63 + np.array([1, 2, 5, 7, 9], dtype="uint64")) - res, lidx, ridx = idx2.join(idx, how="left", return_indexers=True) - - # 1 is in idx2, so it should be x2 - eres = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5, 7, 9], dtype="uint64")) - eridx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) - elidx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) - - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - def test_join_right(self, index_large): - other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) - other_mono = UInt64Index( - 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") - ) - - # not monotonic - res, lidx, ridx = index_large.join(other, how="right", return_indexers=True) - eres = other - elidx = np.array([-1, -1, 4, -1, -1, 1], dtype=np.intp) - - tm.assert_numpy_array_equal(lidx, elidx) - assert isinstance(other, UInt64Index) - tm.assert_index_equal(res, eres) - assert ridx is None - - # monotonic - res, lidx, ridx = index_large.join( - other_mono, how="right", return_indexers=True - ) - eres = other_mono - elidx = np.array([-1, -1, -1, 1, -1, 4], dtype=np.intp) - - assert isinstance(other, UInt64Index) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_index_equal(res, eres) - assert ridx is None - - # non-unique - idx = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5], dtype="uint64")) - idx2 = UInt64Index(2 ** 63 + np.array([1, 2, 5, 7, 9], dtype="uint64")) - res, lidx, ridx = idx.join(idx2, how="right", return_indexers=True) - - # 1 is in idx2, so it should be x2 - eres = UInt64Index(2 ** 63 + np.array([1, 1, 2, 5, 7, 9], dtype="uint64")) - elidx = np.array([0, 1, 2, 3, -1, -1], dtype=np.intp) - eridx = np.array([0, 0, 1, 2, 3, 4], dtype=np.intp) - - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - def test_join_non_int_index(self, index_large): - other = Index( - 2 ** 63 + np.array([1, 5, 7, 10, 20], dtype="uint64"), dtype=object - ) - - outer = index_large.join(other, how="outer") - outer2 = other.join(index_large, how="outer") - expected = Index( - 2 ** 63 + np.array([0, 1, 5, 7, 10, 15, 20, 25], dtype="uint64") - ) - tm.assert_index_equal(outer, outer2) - tm.assert_index_equal(outer, expected) - - inner = index_large.join(other, how="inner") - inner2 = other.join(index_large, how="inner") - expected = Index(2 ** 63 + np.array([10, 20], dtype="uint64")) - tm.assert_index_equal(inner, inner2) - tm.assert_index_equal(inner, expected) - - left = index_large.join(other, how="left") - tm.assert_index_equal(left, index_large.astype(object)) - - left2 = other.join(index_large, how="left") - tm.assert_index_equal(left2, other) - - right = index_large.join(other, how="right") - tm.assert_index_equal(right, other) - - right2 = other.join(index_large, how="right") - tm.assert_index_equal(right2, index_large.astype(object)) - - def test_join_outer(self, index_large): - other = UInt64Index(2 ** 63 + np.array([7, 12, 25, 1, 2, 10], dtype="uint64")) - other_mono = UInt64Index( - 2 ** 63 + np.array([1, 2, 7, 10, 12, 25], dtype="uint64") - ) - - # not monotonic - # guarantee of sortedness - res, lidx, ridx = index_large.join(other, how="outer", return_indexers=True) - noidx_res = index_large.join(other, how="outer") - tm.assert_index_equal(res, noidx_res) - - eres = UInt64Index( - 2 ** 63 + np.array([0, 1, 2, 7, 10, 12, 15, 20, 25], dtype="uint64") - ) - elidx = np.array([0, -1, -1, -1, 1, -1, 2, 3, 4], dtype=np.intp) - eridx = np.array([-1, 3, 4, 0, 5, 1, -1, -1, 2], dtype=np.intp) - - assert isinstance(res, UInt64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - - # monotonic - res, lidx, ridx = index_large.join( - other_mono, how="outer", return_indexers=True - ) - noidx_res = index_large.join(other_mono, how="outer") - tm.assert_index_equal(res, noidx_res) - - elidx = np.array([0, -1, -1, -1, 1, -1, 2, 3, 4], dtype=np.intp) - eridx = np.array([-1, 0, 1, 2, 3, 4, -1, -1, 5], dtype=np.intp) - - assert isinstance(res, UInt64Index) - tm.assert_index_equal(res, eres) - tm.assert_numpy_array_equal(lidx, elidx) - tm.assert_numpy_array_equal(ridx, eridx) - @pytest.mark.parametrize("dtype", ["int64", "uint64"]) def test_int_float_union_dtype(dtype): diff --git a/pandas/tests/indexes/test_setops.py b/pandas/tests/indexes/test_setops.py index abfa413d56655..d0cbb2ab75f72 100644 --- a/pandas/tests/indexes/test_setops.py +++ b/pandas/tests/indexes/test_setops.py @@ -13,7 +13,7 @@ from pandas import Float64Index, Int64Index, RangeIndex, UInt64Index import pandas._testing as tm from pandas.api.types import pandas_dtype -from pandas.tests.indexes.conftest import indices_dict +from pandas.conftest import indices_dict COMPATIBLE_INCONSISTENT_PAIRS = { (Int64Index, RangeIndex): (tm.makeIntIndex, tm.makeRangeIndex), diff --git a/pandas/tests/indexes/timedeltas/test_constructors.py b/pandas/tests/indexes/timedeltas/test_constructors.py index 32e6821e87f05..8e54561df1624 100644 --- a/pandas/tests/indexes/timedeltas/test_constructors.py +++ b/pandas/tests/indexes/timedeltas/test_constructors.py @@ -10,6 +10,12 @@ class TestTimedeltaIndex: + @pytest.mark.parametrize("unit", ["Y", "y", "M"]) + def test_unit_m_y_raises(self, unit): + msg = "Units 'M' and 'Y' are no longer supported" + with pytest.raises(ValueError, match=msg): + TimedeltaIndex([1, 3, 7], unit) + def test_int64_nocopy(self): # GH#23539 check that a copy isn't made when we pass int64 data # and copy=False @@ -149,7 +155,7 @@ def test_constructor(self): def test_constructor_iso(self): # GH #21877 expected = timedelta_range("1s", periods=9, freq="s") - durations = ["P0DT0H0M{}S".format(i) for i in range(1, 10)] + durations = [f"P0DT0H0M{i}S" for i in range(1, 10)] result = to_timedelta(durations) tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/timedeltas/test_join.py b/pandas/tests/indexes/timedeltas/test_join.py new file mode 100644 index 0000000000000..aaf4ef29e162b --- /dev/null +++ b/pandas/tests/indexes/timedeltas/test_join.py @@ -0,0 +1,49 @@ +import numpy as np + +from pandas import Index, Timedelta, timedelta_range +import pandas._testing as tm + + +class TestJoin: + def test_append_join_nondatetimeindex(self): + rng = timedelta_range("1 days", periods=10) + idx = Index(["a", "b", "c", "d"]) + + result = rng.append(idx) + assert isinstance(result[0], Timedelta) + + # it works + rng.join(idx, how="outer") + + def test_join_self(self, join_type): + index = timedelta_range("1 day", periods=10) + joined = index.join(index, how=join_type) + tm.assert_index_equal(index, joined) + + def test_does_not_convert_mixed_integer(self): + df = tm.makeCustomDataframe( + 10, + 10, + data_gen_f=lambda *args, **kwargs: np.random.randn(), + r_idx_type="i", + c_idx_type="td", + ) + str(df) + + cols = df.columns.join(df.index, how="outer") + joined = cols.join(df.columns) + assert cols.dtype == np.dtype("O") + assert cols.dtype == joined.dtype + tm.assert_index_equal(cols, joined) + + def test_join_preserves_freq(self): + # GH#32157 + tdi = timedelta_range("1 day", periods=10) + result = tdi[:5].join(tdi[5:], how="outer") + assert result.freq == tdi.freq + tm.assert_index_equal(result, tdi) + + result = tdi[:5].join(tdi[6:], how="outer") + assert result.freq is None + expected = tdi.delete(5) + tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/timedeltas/test_ops.py b/pandas/tests/indexes/timedeltas/test_ops.py index a3e390fc941c7..6606507dabc29 100644 --- a/pandas/tests/indexes/timedeltas/test_ops.py +++ b/pandas/tests/indexes/timedeltas/test_ops.py @@ -113,15 +113,6 @@ def test_order(self): ["1 day", "3 day", "5 day", "2 day", "1 day"], name="idx2" ) - # TODO(wesm): unused? - # exp2 = TimedeltaIndex(['1 day', '1 day', '2 day', - # '3 day', '5 day'], name='idx2') - - # idx3 = TimedeltaIndex([pd.NaT, '3 minute', '5 minute', - # '2 minute', pd.NaT], name='idx3') - # exp3 = TimedeltaIndex([pd.NaT, pd.NaT, '2 minute', '3 minute', - # '5 minute'], name='idx3') - for idx, expected in [(idx1, exp1), (idx1, exp1), (idx1, exp1)]: ordered = idx.sort_values() tm.assert_index_equal(ordered, expected) @@ -189,9 +180,6 @@ def test_infer_freq(self, freq): tm.assert_index_equal(idx, result) assert result.freq == freq - def test_shift(self): - pass # handled in test_arithmetic.py - def test_repeat(self): index = pd.timedelta_range("1 days", periods=2, freq="D") exp = pd.TimedeltaIndex(["1 days", "1 days", "2 days", "2 days"]) diff --git a/pandas/tests/indexes/timedeltas/test_timedelta.py b/pandas/tests/indexes/timedeltas/test_timedelta.py index 3b52b93fa6369..d4a94f8693081 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta.py @@ -91,27 +91,6 @@ def test_factorize(self): tm.assert_numpy_array_equal(arr, exp_arr) tm.assert_index_equal(idx, idx3) - def test_join_self(self, join_type): - index = timedelta_range("1 day", periods=10) - joined = index.join(index, how=join_type) - tm.assert_index_equal(index, joined) - - def test_does_not_convert_mixed_integer(self): - df = tm.makeCustomDataframe( - 10, - 10, - data_gen_f=lambda *args, **kwargs: randn(), - r_idx_type="i", - c_idx_type="td", - ) - str(df) - - cols = df.columns.join(df.index, how="outer") - joined = cols.join(df.columns) - assert cols.dtype == np.dtype("O") - assert cols.dtype == joined.dtype - tm.assert_index_equal(cols, joined) - def test_sort_values(self): idx = TimedeltaIndex(["4d", "1d", "2d"]) @@ -181,16 +160,6 @@ def test_hash_error(self): ): hash(index) - def test_append_join_nondatetimeindex(self): - rng = timedelta_range("1 days", periods=10) - idx = Index(["a", "b", "c", "d"]) - - result = rng.append(idx) - assert isinstance(result[0], Timedelta) - - # it works - rng.join(idx, how="outer") - def test_append_numpy_bug_1681(self): td = timedelta_range("1 days", "10 days", freq="2D") @@ -284,17 +253,3 @@ def test_freq_conversion(self): result = td.astype("timedelta64[s]") tm.assert_index_equal(result, expected) - - @pytest.mark.parametrize("unit", ["Y", "y", "M"]) - def test_unit_m_y_raises(self, unit): - msg = "Units 'M' and 'Y' are no longer supported" - with pytest.raises(ValueError, match=msg): - TimedeltaIndex([1, 3, 7], unit) - - -class TestTimeSeries: - def test_series_box_timedelta(self): - rng = timedelta_range("1 day 1 s", periods=5, freq="h") - s = Series(rng) - assert isinstance(s[1], Timedelta) - assert isinstance(s.iat[2], Timedelta) diff --git a/pandas/tests/indexes/timedeltas/test_timedelta_range.py b/pandas/tests/indexes/timedeltas/test_timedelta_range.py index 1cef9de6a3a77..9f12af9a96104 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -1,7 +1,6 @@ import numpy as np import pytest -import pandas as pd from pandas import timedelta_range, to_timedelta import pandas._testing as tm @@ -31,23 +30,6 @@ def test_timedelta_range(self): result = timedelta_range("0 days", freq="30T", periods=50) tm.assert_index_equal(result, expected) - # GH 11776 - arr = np.arange(10).reshape(2, 5) - df = pd.DataFrame(np.arange(10).reshape(2, 5)) - for arg in (arr, df): - with pytest.raises(TypeError, match="1-d array"): - to_timedelta(arg) - for errors in ["ignore", "raise", "coerce"]: - with pytest.raises(TypeError, match="1-d array"): - to_timedelta(arg, errors=errors) - - # issue10583 - df = pd.DataFrame(np.random.normal(size=(10, 4))) - df.index = pd.timedelta_range(start="0s", periods=10, freq="s") - expected = df.loc[pd.Timedelta("0s") :, :] - result = df.loc["0s":, :] - tm.assert_frame_equal(expected, result) - @pytest.mark.parametrize( "periods, freq", [(3, "2D"), (5, "D"), (6, "19H12T"), (7, "16H"), (9, "12H")] ) diff --git a/pandas/tests/indexes/timedeltas/test_tools.py b/pandas/tests/indexes/timedeltas/test_tools.py index 477fc092a4e16..e3cf3a7f16a82 100644 --- a/pandas/tests/indexes/timedeltas/test_tools.py +++ b/pandas/tests/indexes/timedeltas/test_tools.py @@ -57,6 +57,17 @@ def test_to_timedelta(self): expected = TimedeltaIndex([np.timedelta64(1, "D")] * 5) tm.assert_index_equal(result, expected) + def test_to_timedelta_dataframe(self): + # GH 11776 + arr = np.arange(10).reshape(2, 5) + df = pd.DataFrame(np.arange(10).reshape(2, 5)) + for arg in (arr, df): + with pytest.raises(TypeError, match="1-d array"): + to_timedelta(arg) + for errors in ["ignore", "raise", "coerce"]: + with pytest.raises(TypeError, match="1-d array"): + to_timedelta(arg, errors=errors) + def test_to_timedelta_invalid(self): # bad value for errors parameter diff --git a/pandas/tests/indexing/common.py b/pandas/tests/indexing/common.py index 3c027b035c2b8..9cc031001f81c 100644 --- a/pandas/tests/indexing/common.py +++ b/pandas/tests/indexing/common.py @@ -1,17 +1,14 @@ """ common utilities """ import itertools -from warnings import catch_warnings import numpy as np -from pandas.core.dtypes.common import is_scalar - from pandas import DataFrame, Float64Index, MultiIndex, Series, UInt64Index, date_range import pandas._testing as tm def _mklbl(prefix, n): - return ["{prefix}{i}".format(prefix=prefix, i=i) for i in range(n)] + return [f"{prefix}{i}" for i in range(n)] def _axify(obj, key, axis): @@ -99,46 +96,24 @@ def setup_method(self, method): for kind in self._kinds: d = dict() for typ in self._typs: - d[typ] = getattr(self, "{kind}_{typ}".format(kind=kind, typ=typ)) + d[typ] = getattr(self, f"{kind}_{typ}") setattr(self, kind, d) def generate_indices(self, f, values=False): - """ generate the indices + """ + generate the indices if values is True , use the axis values is False, use the range """ - axes = f.axes if values: axes = (list(range(len(ax))) for ax in axes) return itertools.product(*axes) - def get_result(self, obj, method, key, axis): - """ return the result for this obj with this key and this axis """ - - if isinstance(key, dict): - key = key[axis] - - # use an artificial conversion to map the key as integers to the labels - # so ix can work for comparisons - if method == "indexer": - method = "ix" - key = obj._get_axis(axis)[key] - - # in case we actually want 0 index slicing - with catch_warnings(record=True): - try: - xp = getattr(obj, method).__getitem__(_axify(obj, key, axis)) - except AttributeError: - xp = getattr(obj, method).__getitem__(key) - - return xp - def get_value(self, name, f, i, values=False): """ return the value for the location i """ - # check against values if values: return f.values[i] @@ -170,45 +145,29 @@ def check_values(self, f, func, values=False): tm.assert_almost_equal(result, expected) def check_result( - self, method1, key1, method2, key2, typs=None, axes=None, fails=None, + self, method, key, typs=None, axes=None, fails=None, ): - def _eq(axis, obj, key1, key2): + def _eq(axis, obj, key): """ compare equal for these 2 keys """ - if axis > obj.ndim - 1: - return - + axified = _axify(obj, key, axis) try: - rs = getattr(obj, method1).__getitem__(_axify(obj, key1, axis)) - - try: - xp = self.get_result(obj=obj, method=method2, key=key2, axis=axis) - except (KeyError, IndexError): - # TODO: why is this allowed? - return - - if is_scalar(rs) and is_scalar(xp): - assert rs == xp - else: - tm.assert_equal(rs, xp) + getattr(obj, method).__getitem__(axified) except (IndexError, TypeError, KeyError) as detail: # if we are in fails, the ok, otherwise raise it if fails is not None: if isinstance(detail, fails): - result = f"ok ({type(detail).__name__})" return - - result = type(detail).__name__ - raise AssertionError(result, detail) + raise if typs is None: typs = self._typs if axes is None: axes = [0, 1] - elif not isinstance(axes, (tuple, list)): - assert isinstance(axes, int) + else: + assert axes in [0, 1] axes = [axes] # check @@ -217,8 +176,8 @@ def _eq(axis, obj, key1, key2): d = getattr(self, kind) for ax in axes: for typ in typs: - if typ not in self._typs: - continue + assert typ in self._typs obj = d[typ] - _eq(axis=ax, obj=obj, key1=key1, key2=key2) + if ax < obj.ndim: + _eq(axis=ax, obj=obj, key=key) diff --git a/pandas/tests/indexing/multiindex/conftest.py b/pandas/tests/indexing/multiindex/conftest.py index e6d5a9eb84410..0256f5e35e1db 100644 --- a/pandas/tests/indexing/multiindex/conftest.py +++ b/pandas/tests/indexing/multiindex/conftest.py @@ -20,8 +20,10 @@ def multiindex_dataframe_random_data(): @pytest.fixture def multiindex_year_month_day_dataframe_random_data(): - """DataFrame with 3 level MultiIndex (year, month, day) covering - first 100 business days from 2000-01-01 with random data""" + """ + DataFrame with 3 level MultiIndex (year, month, day) covering + first 100 business days from 2000-01-01 with random data + """ tdf = tm.makeTimeDataFrame(100) ymd = tdf.groupby([lambda x: x.year, lambda x: x.month, lambda x: x.day]).sum() # use Int64Index, to make sure things work diff --git a/pandas/tests/indexing/test_categorical.py b/pandas/tests/indexing/test_categorical.py index da935b1c911d0..8a8ac584c16c2 100644 --- a/pandas/tests/indexing/test_categorical.py +++ b/pandas/tests/indexing/test_categorical.py @@ -82,11 +82,7 @@ def test_loc_scalar(self): with pytest.raises(TypeError, match=msg): df.loc["d", "C"] = 10 - msg = ( - "cannot do label indexing on CategoricalIndex with these " - r"indexers \[1\] of type int" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^1$"): df.loc[1] def test_getitem_scalar(self): diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py index e845487ffca9a..17722e949df1e 100644 --- a/pandas/tests/indexing/test_chaining_and_caching.py +++ b/pandas/tests/indexing/test_chaining_and_caching.py @@ -346,20 +346,17 @@ def test_chained_getitem_with_lists(self): # GH6394 # Regression in chained getitem indexing with embedded list-like from # 0.12 - def check(result, expected): - tm.assert_numpy_array_equal(result, expected) - assert isinstance(result, np.ndarray) df = DataFrame({"A": 5 * [np.zeros(3)], "B": 5 * [np.ones(3)]}) expected = df["A"].iloc[2] result = df.loc[2, "A"] - check(result, expected) + tm.assert_numpy_array_equal(result, expected) result2 = df.iloc[2]["A"] - check(result2, expected) + tm.assert_numpy_array_equal(result2, expected) result3 = df["A"].loc[2] - check(result3, expected) + tm.assert_numpy_array_equal(result3, expected) result4 = df["A"].iloc[2] - check(result4, expected) + tm.assert_numpy_array_equal(result4, expected) def test_cache_updating(self): # GH 4939, make sure to update the cache on setitem diff --git a/pandas/tests/indexing/test_check_indexer.py b/pandas/tests/indexing/test_check_indexer.py index 82f8c12229824..69d4065234d93 100644 --- a/pandas/tests/indexing/test_check_indexer.py +++ b/pandas/tests/indexing/test_check_indexer.py @@ -34,12 +34,14 @@ def test_valid_input(indexer, expected): @pytest.mark.parametrize( "indexer", [[True, False, None], pd.array([True, False, None], dtype="boolean")], ) -def test_bool_raise_missing_values(indexer): - array = np.array([1, 2, 3]) +def test_boolean_na_returns_indexer(indexer): + # https://github.com/pandas-dev/pandas/issues/31503 + arr = np.array([1, 2, 3]) - msg = "Cannot mask with a boolean indexer containing NA values" - with pytest.raises(ValueError, match=msg): - check_array_indexer(array, indexer) + result = check_array_indexer(arr, indexer) + expected = np.array([True, False, False], dtype=bool) + + tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize( diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index b904755b099d0..bea8eae9bb850 100644 --- a/pandas/tests/indexing/test_coercion.py +++ b/pandas/tests/indexing/test_coercion.py @@ -943,7 +943,7 @@ class TestReplaceSeriesCoercion(CoercionBase): for tz in ["UTC", "US/Eastern"]: # to test tz => different tz replacement - key = "datetime64[ns, {0}]".format(tz) + key = f"datetime64[ns, {tz}]" rep[key] = [ pd.Timestamp("2011-01-01", tz=tz), pd.Timestamp("2011-01-03", tz=tz), @@ -1017,9 +1017,7 @@ def test_replace_series(self, how, to_key, from_key): ): if compat.is_platform_32bit() or compat.is_platform_windows(): - pytest.skip( - "32-bit platform buggy: {0} -> {1}".format(from_key, to_key) - ) + pytest.skip(f"32-bit platform buggy: {from_key} -> {to_key}") # Expected: do not downcast by replacement exp = pd.Series(self.rep[to_key], index=index, name="yyy", dtype=from_key) diff --git a/pandas/tests/indexing/test_floats.py b/pandas/tests/indexing/test_floats.py index 8bb88cd9fd63a..c966962a7c87d 100644 --- a/pandas/tests/indexing/test_floats.py +++ b/pandas/tests/indexing/test_floats.py @@ -1,9 +1,27 @@ +import re + import numpy as np import pytest from pandas import DataFrame, Float64Index, Index, Int64Index, RangeIndex, Series import pandas._testing as tm +# We pass through the error message from numpy +_slice_iloc_msg = re.escape( + "only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) " + "and integer or boolean arrays are valid indices" +) + + +def gen_obj(klass, index): + if klass is Series: + obj = Series(np.arange(len(index)), index=index) + else: + obj = DataFrame( + np.random.randn(len(index), len(index)), index=index, columns=index + ) + return obj + class TestFloatIndexers: def check(self, result, original, indexer, getitem): @@ -52,122 +70,120 @@ def test_scalar_error(self, index_func): with pytest.raises(TypeError, match=msg): s.iloc[3.0] - msg = ( - "cannot do positional indexing on {klass} with these " - r"indexers \[3\.0\] of type float".format(klass=type(i).__name__) - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(IndexError, match=_slice_iloc_msg): s.iloc[3.0] = 0 - def test_scalar_non_numeric(self): - - # GH 4892 - # float_indexers should raise exceptions - # on appropriate Index types & accessors - - for index in [ + @pytest.mark.parametrize( + "index_func", + [ tm.makeStringIndex, tm.makeUnicodeIndex, tm.makeCategoricalIndex, tm.makeDateIndex, tm.makeTimedeltaIndex, tm.makePeriodIndex, - ]: + ], + ) + @pytest.mark.parametrize("klass", [Series, DataFrame]) + def test_scalar_non_numeric(self, index_func, klass): - i = index(5) + # GH 4892 + # float_indexers should raise exceptions + # on appropriate Index types & accessors - for s in [ - Series(np.arange(len(i)), index=i), - DataFrame(np.random.randn(len(i), len(i)), index=i, columns=i), - ]: + i = index_func(5) + s = gen_obj(klass, i) - # getting - for idxr, getitem in [(lambda x: x.iloc, False), (lambda x: x, True)]: - - # gettitem on a DataFrame is a KeyError as it is indexing - # via labels on the columns - if getitem and isinstance(s, DataFrame): - error = KeyError - msg = r"^3(\.0)?$" - else: - error = TypeError - msg = ( - r"cannot do (label|positional) indexing " - r"on {klass} with these indexers \[3\.0\] of " - r"type float|" - "Cannot index by location index with a " - "non-integer key".format(klass=type(i).__name__) - ) - with pytest.raises(error, match=msg): - idxr(s)[3.0] - - # label based can be a TypeError or KeyError - if s.index.inferred_type in { - "categorical", - "string", - "unicode", - "mixed", - }: - error = KeyError - msg = r"^3\.0$" - else: - error = TypeError - msg = ( - r"cannot do label indexing " - r"on {klass} with these indexers \[3\.0\] of " - r"type float".format(klass=type(i).__name__) - ) - with pytest.raises(error, match=msg): - s.loc[3.0] - - # contains - assert 3.0 not in s - - # setting with a float fails with iloc + # getting + for idxr, getitem in [(lambda x: x.iloc, False), (lambda x: x, True)]: + + if getitem: + error = KeyError + msg = r"^3\.0?$" + else: + error = TypeError msg = ( r"cannot do (label|positional) indexing " - r"on {klass} with these indexers \[3\.0\] of " - r"type float".format(klass=type(i).__name__) + fr"on {type(i).__name__} with these indexers \[3\.0\] of " + r"type float|" + "Cannot index by location index with a " + "non-integer key" ) - with pytest.raises(TypeError, match=msg): - s.iloc[3.0] = 0 - - # setting with an indexer - if s.index.inferred_type in ["categorical"]: - # Value or Type Error - pass - elif s.index.inferred_type in ["datetime64", "timedelta64", "period"]: - - # these should prob work - # and are inconsistent between series/dataframe ATM - # for idxr in [lambda x: x]: - # s2 = s.copy() - # - # with pytest.raises(TypeError): - # idxr(s2)[3.0] = 0 - pass + with pytest.raises(error, match=msg): + idxr(s)[3.0] + + # label based can be a TypeError or KeyError + if s.index.inferred_type in { + "categorical", + "string", + "unicode", + "mixed", + "period", + "timedelta64", + "datetime64", + }: + error = KeyError + msg = r"^3\.0$" + else: + error = TypeError + msg = ( + r"cannot do (label|positional) indexing " + fr"on {type(i).__name__} with these indexers \[3\.0\] of " + "type float" + ) + with pytest.raises(error, match=msg): + s.loc[3.0] - else: + # contains + assert 3.0 not in s - s2 = s.copy() - s2.loc[3.0] = 10 - assert s2.index.is_object() + # setting with a float fails with iloc + with pytest.raises(IndexError, match=_slice_iloc_msg): + s.iloc[3.0] = 0 - for idxr in [lambda x: x]: - s2 = s.copy() - idxr(s2)[3.0] = 0 - assert s2.index.is_object() + # setting with an indexer + if s.index.inferred_type in ["categorical"]: + # Value or Type Error + pass + elif s.index.inferred_type in ["datetime64", "timedelta64", "period"]: + + # these should prob work + # and are inconsistent between series/dataframe ATM + # for idxr in [lambda x: x]: + # s2 = s.copy() + # + # with pytest.raises(TypeError): + # idxr(s2)[3.0] = 0 + pass - # fallsback to position selection, series only - s = Series(np.arange(len(i)), index=i) - s[3] - msg = ( - r"cannot do label indexing " - r"on {klass} with these indexers \[3\.0\] of " - r"type float".format(klass=type(i).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[3.0] + else: + + s2 = s.copy() + s2.loc[3.0] = 10 + assert s2.index.is_object() + + s2 = s.copy() + s2[3.0] = 0 + assert s2.index.is_object() + + @pytest.mark.parametrize( + "index_func", + [ + tm.makeStringIndex, + tm.makeUnicodeIndex, + tm.makeCategoricalIndex, + tm.makeDateIndex, + tm.makeTimedeltaIndex, + tm.makePeriodIndex, + ], + ) + def test_scalar_non_numeric_series_fallback(self, index_func): + # fallsback to position selection, series only + i = index_func(5) + s = Series(np.arange(len(i)), index=i) + s[3] + with pytest.raises(KeyError, match="^3.0$"): + s[3.0] def test_scalar_with_mixed(self): @@ -176,18 +192,16 @@ def test_scalar_with_mixed(self): # lookup in a pure stringstr # with an invalid indexer - for idxr in [lambda x: x, lambda x: x.iloc]: - - msg = ( - r"cannot do label indexing " - r"on {klass} with these indexers \[1\.0\] of " - r"type float|" - "Cannot index by location index with a non-integer key".format( - klass=Index.__name__ - ) - ) - with pytest.raises(TypeError, match=msg): - idxr(s2)[1.0] + msg = ( + r"cannot do label indexing " + r"on Index with these indexers \[1\.0\] of " + r"type float|" + "Cannot index by location index with a non-integer key" + ) + with pytest.raises(KeyError, match="^1.0$"): + s2[1.0] + with pytest.raises(TypeError, match=msg): + s2.iloc[1.0] with pytest.raises(KeyError, match=r"^1\.0$"): s2.loc[1.0] @@ -198,19 +212,12 @@ def test_scalar_with_mixed(self): # mixed index so we have label # indexing - for idxr in [lambda x: x]: - - msg = ( - r"cannot do label indexing " - r"on {klass} with these indexers \[1\.0\] of " - r"type float".format(klass=Index.__name__) - ) - with pytest.raises(TypeError, match=msg): - idxr(s3)[1.0] + with pytest.raises(KeyError, match="^1.0$"): + s3[1.0] - result = idxr(s3)[1] - expected = 2 - assert result == expected + result = s3[1] + expected = 2 + assert result == expected msg = "Cannot index by location index with a non-integer key" with pytest.raises(TypeError, match=msg): @@ -222,168 +229,151 @@ def test_scalar_with_mixed(self): expected = 3 assert result == expected - def test_scalar_integer(self): + @pytest.mark.parametrize( + "index_func", [tm.makeIntIndex, tm.makeRangeIndex], + ) + @pytest.mark.parametrize("klass", [Series, DataFrame]) + def test_scalar_integer(self, index_func, klass): # test how scalar float indexers work on int indexes # integer index - for i in [Int64Index(range(5)), RangeIndex(5)]: - - for s in [ - Series(np.arange(len(i))), - DataFrame(np.random.randn(len(i), len(i)), index=i, columns=i), - ]: + i = index_func(5) + obj = gen_obj(klass, i) - # coerce to equal int - for idxr, getitem in [(lambda x: x.loc, False), (lambda x: x, True)]: + # coerce to equal int + for idxr, getitem in [(lambda x: x.loc, False), (lambda x: x, True)]: - result = idxr(s)[3.0] - self.check(result, s, 3, getitem) + result = idxr(obj)[3.0] + self.check(result, obj, 3, getitem) - # coerce to equal int - for idxr, getitem in [(lambda x: x.loc, False), (lambda x: x, True)]: + # coerce to equal int + for idxr, getitem in [(lambda x: x.loc, False), (lambda x: x, True)]: - if isinstance(s, Series): + if isinstance(obj, Series): - def compare(x, y): - assert x == y + def compare(x, y): + assert x == y - expected = 100 - else: - compare = tm.assert_series_equal - if getitem: - expected = Series(100, index=range(len(s)), name=3) - else: - expected = Series(100.0, index=range(len(s)), name=3) + expected = 100 + else: + compare = tm.assert_series_equal + if getitem: + expected = Series(100, index=range(len(obj)), name=3) + else: + expected = Series(100.0, index=range(len(obj)), name=3) - s2 = s.copy() - idxr(s2)[3.0] = 100 + s2 = obj.copy() + idxr(s2)[3.0] = 100 - result = idxr(s2)[3.0] - compare(result, expected) + result = idxr(s2)[3.0] + compare(result, expected) - result = idxr(s2)[3] - compare(result, expected) + result = idxr(s2)[3] + compare(result, expected) - # contains - # coerce to equal int - assert 3.0 in s + # contains + # coerce to equal int + assert 3.0 in obj - def test_scalar_float(self): + @pytest.mark.parametrize("klass", [Series, DataFrame]) + def test_scalar_float(self, klass): # scalar float indexers work on a float index index = Index(np.arange(5.0)) - for s in [ - Series(np.arange(len(index)), index=index), - DataFrame( - np.random.randn(len(index), len(index)), index=index, columns=index - ), - ]: + s = gen_obj(klass, index) - # assert all operations except for iloc are ok - indexer = index[3] - for idxr, getitem in [(lambda x: x.loc, False), (lambda x: x, True)]: + # assert all operations except for iloc are ok + indexer = index[3] + for idxr, getitem in [(lambda x: x.loc, False), (lambda x: x, True)]: - # getting - result = idxr(s)[indexer] - self.check(result, s, 3, getitem) + # getting + result = idxr(s)[indexer] + self.check(result, s, 3, getitem) - # setting - s2 = s.copy() - - result = idxr(s2)[indexer] - self.check(result, s, 3, getitem) + # setting + s2 = s.copy() - # random integer is a KeyError - with pytest.raises(KeyError, match=r"^3\.5$"): - idxr(s)[3.5] + result = idxr(s2)[indexer] + self.check(result, s, 3, getitem) - # contains - assert 3.0 in s + # random float is a KeyError + with pytest.raises(KeyError, match=r"^3\.5$"): + idxr(s)[3.5] - # iloc succeeds with an integer - expected = s.iloc[3] - s2 = s.copy() + # contains + assert 3.0 in s - s2.iloc[3] = expected - result = s2.iloc[3] - self.check(result, s, 3, False) + # iloc succeeds with an integer + expected = s.iloc[3] + s2 = s.copy() - # iloc raises with a float - msg = "Cannot index by location index with a non-integer key" - with pytest.raises(TypeError, match=msg): - s.iloc[3.0] + s2.iloc[3] = expected + result = s2.iloc[3] + self.check(result, s, 3, False) - msg = ( - r"cannot do positional indexing " - r"on {klass} with these indexers \[3\.0\] of " - r"type float".format(klass=Float64Index.__name__) - ) - with pytest.raises(TypeError, match=msg): - s2.iloc[3.0] = 0 + # iloc raises with a float + msg = "Cannot index by location index with a non-integer key" + with pytest.raises(TypeError, match=msg): + s.iloc[3.0] - def test_slice_non_numeric(self): + with pytest.raises(IndexError, match=_slice_iloc_msg): + s2.iloc[3.0] = 0 - # GH 4892 - # float_indexers should raise exceptions - # on appropriate Index types & accessors - - for index in [ + @pytest.mark.parametrize( + "index_func", + [ tm.makeStringIndex, tm.makeUnicodeIndex, tm.makeDateIndex, tm.makeTimedeltaIndex, tm.makePeriodIndex, - ]: + ], + ) + @pytest.mark.parametrize("l", [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]) + @pytest.mark.parametrize("klass", [Series, DataFrame]) + def test_slice_non_numeric(self, index_func, l, klass): - index = index(5) - for s in [ - Series(range(5), index=index), - DataFrame(np.random.randn(5, 2), index=index), - ]: + # GH 4892 + # float_indexers should raise exceptions + # on appropriate Index types & accessors + + index = index_func(5) + s = gen_obj(klass, index) + + # getitem + msg = ( + "cannot do positional indexing " + fr"on {type(index).__name__} with these indexers \[(3|4)\.0\] of " + "type float" + ) + with pytest.raises(TypeError, match=msg): + s.iloc[l] - # getitem - for l in [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]: - - msg = ( - "cannot do positional indexing " - r"on {klass} with these indexers \[(3|4)\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s.iloc[l] - - for idxr in [lambda x: x.loc, lambda x: x.iloc, lambda x: x]: - - msg = ( - "cannot do (slice|positional) indexing " - r"on {klass} with these indexers " - r"\[(3|4)(\.0)?\] " - r"of type (float|int)".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - idxr(s)[l] - - # setitem - for l in [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]: - - msg = ( - "cannot do positional indexing " - r"on {klass} with these indexers \[(3|4)\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s.iloc[l] = 0 - - for idxr in [lambda x: x.loc, lambda x: x.iloc, lambda x: x]: - msg = ( - "cannot do (slice|positional) indexing " - r"on {klass} with these indexers " - r"\[(3|4)(\.0)?\] " - r"of type (float|int)".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - idxr(s)[l] = 0 + msg = ( + "cannot do (slice|positional) indexing " + fr"on {type(index).__name__} with these indexers " + r"\[(3|4)(\.0)?\] " + r"of type (float|int)" + ) + for idxr in [lambda x: x.loc, lambda x: x.iloc, lambda x: x]: + with pytest.raises(TypeError, match=msg): + idxr(s)[l] + + # setitem + msg = "slice indices must be integers or None or have an __index__ method" + with pytest.raises(TypeError, match=msg): + s.iloc[l] = 0 + + msg = ( + "cannot do (slice|positional) indexing " + fr"on {type(index).__name__} with these indexers " + r"\[(3|4)(\.0)?\] " + r"of type (float|int)" + ) + for idxr in [lambda x: x.loc, lambda x: x]: + with pytest.raises(TypeError, match=msg): + idxr(s)[l] = 0 def test_slice_integer(self): @@ -403,48 +393,36 @@ def test_slice_integer(self): # getitem for l in [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]: - for idxr in [lambda x: x.loc]: + result = s.loc[l] - result = idxr(s)[l] - - # these are all label indexing - # except getitem which is positional - # empty - if oob: - indexer = slice(0, 0) - else: - indexer = slice(3, 5) - self.check(result, s, indexer, False) - - # positional indexing - msg = ( - "cannot do slice indexing " - r"on {klass} with these indexers \[(3|4)\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[l] + # these are all label indexing + # except getitem which is positional + # empty + if oob: + indexer = slice(0, 0) + else: + indexer = slice(3, 5) + self.check(result, s, indexer, False) # getitem out-of-bounds for l in [slice(-6, 6), slice(-6.0, 6.0)]: - for idxr in [lambda x: x.loc]: - result = idxr(s)[l] + result = s.loc[l] - # these are all label indexing - # except getitem which is positional - # empty - if oob: - indexer = slice(0, 0) - else: - indexer = slice(-6, 6) - self.check(result, s, indexer, False) + # these are all label indexing + # except getitem which is positional + # empty + if oob: + indexer = slice(0, 0) + else: + indexer = slice(-6, 6) + self.check(result, s, indexer, False) # positional indexing msg = ( "cannot do slice indexing " - r"on {klass} with these indexers \[-6\.0\] of " - "type float".format(klass=type(index).__name__) + fr"on {type(index).__name__} with these indexers \[-6\.0\] of " + "type float" ) with pytest.raises(TypeError, match=msg): s[slice(-6.0, 6.0)] @@ -456,169 +434,155 @@ def test_slice_integer(self): (slice(2.5, 3.5), slice(3, 4)), ]: - for idxr in [lambda x: x.loc]: - - result = idxr(s)[l] - if oob: - res = slice(0, 0) - else: - res = res1 + result = s.loc[l] + if oob: + res = slice(0, 0) + else: + res = res1 - self.check(result, s, res, False) + self.check(result, s, res, False) # positional indexing msg = ( "cannot do slice indexing " - r"on {klass} with these indexers \[(2|3)\.5\] of " - "type float".format(klass=type(index).__name__) + fr"on {type(index).__name__} with these indexers \[(2|3)\.5\] of " + "type float" ) with pytest.raises(TypeError, match=msg): s[l] - # setitem - for l in [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]: - - for idxr in [lambda x: x.loc]: - sc = s.copy() - idxr(sc)[l] = 0 - result = idxr(sc)[l].values.ravel() - assert (result == 0).all() - - # positional indexing - msg = ( - "cannot do slice indexing " - r"on {klass} with these indexers \[(3|4)\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[l] = 0 - - def test_integer_positional_indexing(self): + @pytest.mark.parametrize("l", [slice(2, 4.0), slice(2.0, 4), slice(2.0, 4.0)]) + def test_integer_positional_indexing(self, l): """ make sure that we are raising on positional indexing - w.r.t. an integer index """ - + w.r.t. an integer index + """ s = Series(range(2, 6), index=range(2, 6)) result = s[2:4] expected = s.iloc[2:4] tm.assert_series_equal(result, expected) - for idxr in [lambda x: x, lambda x: x.iloc]: + klass = RangeIndex + msg = ( + "cannot do (slice|positional) indexing " + fr"on {klass.__name__} with these indexers \[(2|4)\.0\] of " + "type float" + ) + with pytest.raises(TypeError, match=msg): + s[l] + with pytest.raises(TypeError, match=msg): + s.iloc[l] - for l in [slice(2, 4.0), slice(2.0, 4), slice(2.0, 4.0)]: + @pytest.mark.parametrize( + "index_func", [tm.makeIntIndex, tm.makeRangeIndex], + ) + def test_slice_integer_frame_getitem(self, index_func): - klass = RangeIndex - msg = ( - "cannot do (slice|positional) indexing " - r"on {klass} with these indexers \[(2|4)\.0\] of " - "type float".format(klass=klass.__name__) - ) - with pytest.raises(TypeError, match=msg): - idxr(s)[l] + # similar to above, but on the getitem dim (of a DataFrame) + index = index_func(5) - def test_slice_integer_frame_getitem(self): + s = DataFrame(np.random.randn(5, 2), index=index) - # similar to above, but on the getitem dim (of a DataFrame) - for index in [Int64Index(range(5)), RangeIndex(5)]: + # getitem + for l in [slice(0.0, 1), slice(0, 1.0), slice(0.0, 1.0)]: - s = DataFrame(np.random.randn(5, 2), index=index) + result = s.loc[l] + indexer = slice(0, 2) + self.check(result, s, indexer, False) - def f(idxr): + # positional indexing + msg = ( + "cannot do slice indexing " + fr"on {type(index).__name__} with these indexers \[(0|1)\.0\] of " + "type float" + ) + with pytest.raises(TypeError, match=msg): + s[l] - # getitem - for l in [slice(0.0, 1), slice(0, 1.0), slice(0.0, 1.0)]: + # getitem out-of-bounds + for l in [slice(-10, 10), slice(-10.0, 10.0)]: - result = idxr(s)[l] - indexer = slice(0, 2) - self.check(result, s, indexer, False) + result = s.loc[l] + self.check(result, s, slice(-10, 10), True) - # positional indexing - msg = ( - "cannot do slice indexing " - r"on {klass} with these indexers \[(0|1)\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[l] + # positional indexing + msg = ( + "cannot do slice indexing " + fr"on {type(index).__name__} with these indexers \[-10\.0\] of " + "type float" + ) + with pytest.raises(TypeError, match=msg): + s[slice(-10.0, 10.0)] - # getitem out-of-bounds - for l in [slice(-10, 10), slice(-10.0, 10.0)]: + # getitem odd floats + for l, res in [ + (slice(0.5, 1), slice(1, 2)), + (slice(0, 0.5), slice(0, 1)), + (slice(0.5, 1.5), slice(1, 2)), + ]: - result = idxr(s)[l] - self.check(result, s, slice(-10, 10), True) + result = s.loc[l] + self.check(result, s, res, False) - # positional indexing - msg = ( - "cannot do slice indexing " - r"on {klass} with these indexers \[-10\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[slice(-10.0, 10.0)] - - # getitem odd floats - for l, res in [ - (slice(0.5, 1), slice(1, 2)), - (slice(0, 0.5), slice(0, 1)), - (slice(0.5, 1.5), slice(1, 2)), - ]: - - result = idxr(s)[l] - self.check(result, s, res, False) - - # positional indexing - msg = ( - "cannot do slice indexing " - r"on {klass} with these indexers \[0\.5\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[l] - - # setitem - for l in [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]: - - sc = s.copy() - idxr(sc)[l] = 0 - result = idxr(sc)[l].values.ravel() - assert (result == 0).all() - - # positional indexing - msg = ( - "cannot do slice indexing " - r"on {klass} with these indexers \[(3|4)\.0\] of " - "type float".format(klass=type(index).__name__) - ) - with pytest.raises(TypeError, match=msg): - s[l] = 0 - - f(lambda x: x.loc) - - def test_slice_float(self): + # positional indexing + msg = ( + "cannot do slice indexing " + fr"on {type(index).__name__} with these indexers \[0\.5\] of " + "type float" + ) + with pytest.raises(TypeError, match=msg): + s[l] + + @pytest.mark.parametrize("l", [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]) + @pytest.mark.parametrize( + "index_func", [tm.makeIntIndex, tm.makeRangeIndex], + ) + def test_float_slice_getitem_with_integer_index_raises(self, l, index_func): + + # similar to above, but on the getitem dim (of a DataFrame) + index = index_func(5) + + s = DataFrame(np.random.randn(5, 2), index=index) + + # setitem + sc = s.copy() + sc.loc[l] = 0 + result = sc.loc[l].values.ravel() + assert (result == 0).all() + + # positional indexing + msg = ( + "cannot do slice indexing " + fr"on {type(index).__name__} with these indexers \[(3|4)\.0\] of " + "type float" + ) + with pytest.raises(TypeError, match=msg): + s[l] = 0 + + with pytest.raises(TypeError, match=msg): + s[l] + + @pytest.mark.parametrize("l", [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]) + @pytest.mark.parametrize("klass", [Series, DataFrame]) + def test_slice_float(self, l, klass): # same as above, but for floats index = Index(np.arange(5.0)) + 0.1 - for s in [ - Series(range(5), index=index), - DataFrame(np.random.randn(5, 2), index=index), - ]: + s = gen_obj(klass, index) - for l in [slice(3.0, 4), slice(3, 4.0), slice(3.0, 4.0)]: + expected = s.iloc[3:4] + for idxr in [lambda x: x.loc, lambda x: x]: + + # getitem + result = idxr(s)[l] + assert isinstance(result, type(s)) + tm.assert_equal(result, expected) - expected = s.iloc[3:4] - for idxr in [lambda x: x.loc, lambda x: x]: - - # getitem - result = idxr(s)[l] - if isinstance(s, Series): - tm.assert_series_equal(result, expected) - else: - tm.assert_frame_equal(result, expected) - # setitem - s2 = s.copy() - idxr(s2)[l] = 0 - result = idxr(s2)[l].values.ravel() - assert (result == 0).all() + # setitem + s2 = s.copy() + idxr(s2)[l] = 0 + result = idxr(s2)[l].values.ravel() + assert (result == 0).all() def test_floating_index_doc_example(self): diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py index 08ea4c1579ef8..683d4f2605712 100644 --- a/pandas/tests/indexing/test_iloc.py +++ b/pandas/tests/indexing/test_iloc.py @@ -18,8 +18,6 @@ class TestiLoc(Base): def test_iloc_getitem_int(self): # integer self.check_result( - "iloc", - 2, "iloc", 2, typs=["labels", "mixed", "ts", "floats", "empty"], @@ -29,8 +27,6 @@ def test_iloc_getitem_int(self): def test_iloc_getitem_neg_int(self): # neg integer self.check_result( - "iloc", - -1, "iloc", -1, typs=["labels", "mixed", "ts", "floats", "empty"], @@ -39,8 +35,6 @@ def test_iloc_getitem_neg_int(self): def test_iloc_getitem_list_int(self): self.check_result( - "iloc", - [0, 1, 2], "iloc", [0, 1, 2], typs=["labels", "mixed", "ts", "floats", "empty"], @@ -53,6 +47,17 @@ def test_iloc_getitem_list_int(self): class TestiLoc2: # TODO: better name, just separating out things that dont rely on base class + + def test_is_scalar_access(self): + # GH#32085 index with duplicates doesnt matter for _is_scalar_access + index = pd.Index([1, 2, 1]) + ser = pd.Series(range(3), index=index) + + assert ser.iloc._is_scalar_access((1,)) + + df = ser.to_frame() + assert df.iloc._is_scalar_access((1, 0,)) + def test_iloc_exceeds_bounds(self): # GH6296 @@ -252,9 +257,7 @@ def test_iloc_getitem_bool(self): def test_iloc_getitem_bool_diff_len(self, index): # GH26658 s = Series([1, 2, 3]) - msg = "Boolean index has wrong length: {} instead of {}".format( - len(index), len(s) - ) + msg = f"Boolean index has wrong length: {len(index)} instead of {len(s)}" with pytest.raises(IndexError, match=msg): _ = s.iloc[index] @@ -618,9 +621,7 @@ def test_iloc_mask(self): r = expected.get(key) if r != ans: raise AssertionError( - "[{key}] does not match [{ans}], received [{r}]".format( - key=key, ans=ans, r=r - ) + f"[{key}] does not match [{ans}], received [{r}]" ) def test_iloc_non_unique_indexing(self): diff --git a/pandas/tests/indexing/test_indexers.py b/pandas/tests/indexing/test_indexers.py new file mode 100644 index 0000000000000..173f33b19f8d5 --- /dev/null +++ b/pandas/tests/indexing/test_indexers.py @@ -0,0 +1,11 @@ +# Tests aimed at pandas.core.indexers +import numpy as np + +from pandas.core.indexers import length_of_indexer + + +def test_length_of_indexer(): + arr = np.zeros(4, dtype=bool) + arr[0] = 1 + result = length_of_indexer(arr) + assert result == 1 diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 98940b64330b4..8af0fe548e48a 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -7,14 +7,11 @@ import numpy as np import pytest -from pandas.errors import AbstractMethodError - from pandas.core.dtypes.common import is_float_dtype, is_integer_dtype import pandas as pd from pandas import DataFrame, Index, NaT, Series import pandas._testing as tm -from pandas.core.generic import NDFrame from pandas.core.indexers import validate_indices from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice from pandas.tests.indexing.common import _mklbl @@ -80,33 +77,18 @@ def test_getitem_ndarray_3d(self, index, obj, idxr, idxr_id): idxr = idxr(obj) nd3 = np.random.randint(5, size=(2, 2, 2)) - msg = ( - r"Buffer has wrong number of dimensions \(expected 1, " - r"got 3\)|" - "Cannot index with multidimensional key|" - r"Wrong number of dimensions. values.ndim != ndim \[3 != 1\]|" - "Index data must be 1-dimensional" + msg = "|".join( + [ + r"Buffer has wrong number of dimensions \(expected 1, got 3\)", + "Cannot index with multidimensional key", + r"Wrong number of dimensions. values.ndim != ndim \[3 != 1\]", + "Index data must be 1-dimensional", + ] ) - if ( - isinstance(obj, Series) - and idxr_id == "getitem" - and index.inferred_type - in [ - "string", - "datetime64", - "period", - "timedelta64", - "boolean", - "categorical", - ] - ): + with pytest.raises(ValueError, match=msg): with tm.assert_produces_warning(DeprecationWarning, check_stacklevel=False): idxr[nd3] - else: - with pytest.raises(ValueError, match=msg): - with tm.assert_produces_warning(DeprecationWarning): - idxr[nd3] @pytest.mark.parametrize( "index", tm.all_index_generator(5), ids=lambda x: type(x).__name__ @@ -133,38 +115,24 @@ def test_setitem_ndarray_3d(self, index, obj, idxr, idxr_id): idxr = idxr(obj) nd3 = np.random.randint(5, size=(2, 2, 2)) - msg = ( - r"Buffer has wrong number of dimensions \(expected 1, " - r"got 3\)|" - "'pandas._libs.interval.IntervalTree' object has no attribute " - "'get_loc'|" # AttributeError - "unhashable type: 'numpy.ndarray'|" # TypeError - "No matching signature found|" # TypeError - r"^\[\[\[|" # pandas.core.indexing.IndexingError - "Index data must be 1-dimensional" - ) - - if (idxr_id == "iloc") or ( - ( - isinstance(obj, Series) - and idxr_id == "setitem" - and index.inferred_type - in [ - "floating", - "string", - "datetime64", - "period", - "timedelta64", - "boolean", - "categorical", - ] - ) + if idxr_id == "iloc": + err = ValueError + msg = f"Cannot set values with ndim > {obj.ndim}" + elif ( + isinstance(index, pd.IntervalIndex) + and idxr_id == "setitem" + and obj.ndim == 1 ): - idxr[nd3] = 0 + err = AttributeError + msg = ( + "'pandas._libs.interval.IntervalTree' object has no attribute 'get_loc'" + ) else: - err = (ValueError, AttributeError) - with pytest.raises(err, match=msg): - idxr[nd3] = 0 + err = ValueError + msg = r"Buffer has wrong number of dimensions \(expected 1, got 3\)|" + + with pytest.raises(err, match=msg): + idxr[nd3] = 0 def test_inf_upcast(self): # GH 16957 @@ -543,7 +511,7 @@ def __init__(self, value): self.value = value def __str__(self) -> str: - return "[{0}]".format(self.value) + return f"[{self.value}]" __repr__ = __str__ @@ -1123,29 +1091,6 @@ def test_extension_array_cross_section_converts(): tm.assert_series_equal(result, expected) -@pytest.mark.parametrize( - "idxr, error, error_message", - [ - (lambda x: x, AbstractMethodError, None), - ( - lambda x: x.loc, - AttributeError, - "type object 'NDFrame' has no attribute '_AXIS_NAMES'", - ), - ( - lambda x: x.iloc, - AttributeError, - "type object 'NDFrame' has no attribute '_AXIS_NAMES'", - ), - ], -) -def test_ndframe_indexing_raises(idxr, error, error_message): - # GH 25567 - frame = NDFrame(np.random.randint(5, size=(2, 2, 2))) - with pytest.raises(error, match=error_message): - idxr(frame)[0] - - def test_readonly_indices(): # GH#17192 iloc with read-only array raising TypeError df = pd.DataFrame({"data": np.ones(100, dtype="float64")}) diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 3a726fb9923ee..4d042af8d59b4 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -16,32 +16,27 @@ class TestLoc(Base): def test_loc_getitem_int(self): # int label - self.check_result("loc", 2, "loc", 2, typs=["label"], fails=KeyError) + self.check_result("loc", 2, typs=["labels"], fails=KeyError) def test_loc_getitem_label(self): # label - self.check_result("loc", "c", "loc", "c", typs=["empty"], fails=KeyError) + self.check_result("loc", "c", typs=["empty"], fails=KeyError) def test_loc_getitem_label_out_of_range(self): # out of range label self.check_result( - "loc", - "f", - "loc", - "f", - typs=["ints", "uints", "labels", "mixed", "ts"], - fails=KeyError, + "loc", "f", typs=["ints", "uints", "labels", "mixed", "ts"], fails=KeyError, ) - self.check_result("loc", "f", "ix", "f", typs=["floats"], fails=KeyError) - self.check_result("loc", "f", "loc", "f", typs=["floats"], fails=KeyError) + self.check_result("loc", "f", typs=["floats"], fails=KeyError) + self.check_result("loc", "f", typs=["floats"], fails=KeyError) self.check_result( - "loc", 20, "loc", 20, typs=["ints", "uints", "mixed"], fails=KeyError, + "loc", 20, typs=["ints", "uints", "mixed"], fails=KeyError, ) - self.check_result("loc", 20, "loc", 20, typs=["labels"], fails=TypeError) - self.check_result("loc", 20, "loc", 20, typs=["ts"], axes=0, fails=TypeError) - self.check_result("loc", 20, "loc", 20, typs=["floats"], axes=0, fails=KeyError) + self.check_result("loc", 20, typs=["labels"], fails=KeyError) + self.check_result("loc", 20, typs=["ts"], axes=0, fails=KeyError) + self.check_result("loc", 20, typs=["floats"], axes=0, fails=KeyError) def test_loc_getitem_label_list(self): # TODO: test something here? @@ -50,49 +45,25 @@ def test_loc_getitem_label_list(self): def test_loc_getitem_label_list_with_missing(self): self.check_result( - "loc", [0, 1, 2], "loc", [0, 1, 2], typs=["empty"], fails=KeyError, + "loc", [0, 1, 2], typs=["empty"], fails=KeyError, ) self.check_result( - "loc", - [0, 2, 10], - "ix", - [0, 2, 10], - typs=["ints", "uints", "floats"], - axes=0, - fails=KeyError, + "loc", [0, 2, 10], typs=["ints", "uints", "floats"], axes=0, fails=KeyError, ) self.check_result( - "loc", - [3, 6, 7], - "ix", - [3, 6, 7], - typs=["ints", "uints", "floats"], - axes=1, - fails=KeyError, + "loc", [3, 6, 7], typs=["ints", "uints", "floats"], axes=1, fails=KeyError, ) # GH 17758 - MultiIndex and missing keys self.check_result( - "loc", - [(1, 3), (1, 4), (2, 5)], - "ix", - [(1, 3), (1, 4), (2, 5)], - typs=["multi"], - axes=0, - fails=KeyError, + "loc", [(1, 3), (1, 4), (2, 5)], typs=["multi"], axes=0, fails=KeyError, ) def test_loc_getitem_label_list_fails(self): # fails self.check_result( - "loc", - [20, 30, 40], - "loc", - [20, 30, 40], - typs=["ints", "uints"], - axes=1, - fails=KeyError, + "loc", [20, 30, 40], typs=["ints", "uints"], axes=1, fails=KeyError, ) def test_loc_getitem_label_array_like(self): @@ -104,7 +75,7 @@ def test_loc_getitem_bool(self): # boolean indexers b = [True, False, True, False] - self.check_result("loc", b, "loc", b, typs=["empty"], fails=IndexError) + self.check_result("loc", b, typs=["empty"], fails=IndexError) def test_loc_getitem_label_slice(self): @@ -115,8 +86,6 @@ def test_loc_getitem_label_slice(self): # GH 14316 self.check_result( - "loc", - slice(1, 3), "loc", slice(1, 3), typs=["labels", "mixed", "empty", "ts", "floats"], @@ -124,42 +93,18 @@ def test_loc_getitem_label_slice(self): ) self.check_result( - "loc", - slice("20130102", "20130104"), - "loc", - slice("20130102", "20130104"), - typs=["ts"], - axes=1, - fails=TypeError, + "loc", slice("20130102", "20130104"), typs=["ts"], axes=1, fails=TypeError, ) self.check_result( - "loc", - slice(2, 8), - "loc", - slice(2, 8), - typs=["mixed"], - axes=0, - fails=TypeError, + "loc", slice(2, 8), typs=["mixed"], axes=0, fails=TypeError, ) self.check_result( - "loc", - slice(2, 8), - "loc", - slice(2, 8), - typs=["mixed"], - axes=1, - fails=KeyError, + "loc", slice(2, 8), typs=["mixed"], axes=1, fails=KeyError, ) self.check_result( - "loc", - slice(2, 4, 2), - "loc", - slice(2, 4, 2), - typs=["mixed"], - axes=0, - fails=TypeError, + "loc", slice(2, 4, 2), typs=["mixed"], axes=0, fails=TypeError, ) @@ -272,9 +217,7 @@ def test_getitem_label_list_with_missing(self): def test_loc_getitem_bool_diff_len(self, index): # GH26658 s = Series([1, 2, 3]) - msg = "Boolean index has wrong length: {} instead of {}".format( - len(index), len(s) - ) + msg = f"Boolean index has wrong length: {len(index)} instead of {len(s)}" with pytest.raises(IndexError, match=msg): _ = s.loc[index] @@ -539,12 +482,8 @@ def test_loc_assign_non_ns_datetime(self, unit): } ) - df.loc[:, unit] = df.loc[:, "timestamp"].values.astype( - "datetime64[{unit}]".format(unit=unit) - ) - df["expected"] = df.loc[:, "timestamp"].values.astype( - "datetime64[{unit}]".format(unit=unit) - ) + df.loc[:, unit] = df.loc[:, "timestamp"].values.astype(f"datetime64[{unit}]") + df["expected"] = df.loc[:, "timestamp"].values.astype(f"datetime64[{unit}]") expected = Series(df.loc[:, "expected"], name=unit) tm.assert_series_equal(df.loc[:, unit], expected) @@ -1028,3 +967,11 @@ def test_loc_set_dataframe_multiindex(): result = expected.copy() result.loc[0, [(0, 1)]] = result.loc[0, [(0, 1)]] tm.assert_frame_equal(result, expected) + + +def test_loc_mixed_int_float(): + # GH#19456 + ser = pd.Series(range(2), pd.Index([1, 2.0], dtype=object)) + + result = ser.loc[1] + assert result == 0 diff --git a/pandas/tests/indexing/test_na_indexing.py b/pandas/tests/indexing/test_na_indexing.py index befe4fee8ecf8..345ca30ec77eb 100644 --- a/pandas/tests/indexing/test_na_indexing.py +++ b/pandas/tests/indexing/test_na_indexing.py @@ -62,18 +62,29 @@ def test_series_mask_boolean(values, dtype, mask, box_mask, frame): @pytest.mark.parametrize("frame", [True, False]) -def test_indexing_with_na_raises(frame): +def test_na_treated_as_false(frame): + # https://github.com/pandas-dev/pandas/issues/31503 s = pd.Series([1, 2, 3], name="name") if frame: s = s.to_frame() + mask = pd.array([True, False, None], dtype="boolean") - match = "cannot mask with array containing NA / NaN values" - with pytest.raises(ValueError, match=match): - s[mask] - with pytest.raises(ValueError, match=match): - s.loc[mask] + result = s[mask] + expected = s[mask.fillna(False)] + + result_loc = s.loc[mask] + expected_loc = s.loc[mask.fillna(False)] - with pytest.raises(ValueError, match=match): - s.iloc[mask] + result_iloc = s.iloc[mask] + expected_iloc = s.iloc[mask.fillna(False)] + + if frame: + tm.assert_frame_equal(result, expected) + tm.assert_frame_equal(result_loc, expected_loc) + tm.assert_frame_equal(result_iloc, expected_iloc) + else: + tm.assert_series_equal(result, expected) + tm.assert_series_equal(result_loc, expected_loc) + tm.assert_series_equal(result_iloc, expected_iloc) diff --git a/pandas/tests/indexing/test_scalar.py b/pandas/tests/indexing/test_scalar.py index 3622b12b853a4..25939e63c256b 100644 --- a/pandas/tests/indexing/test_scalar.py +++ b/pandas/tests/indexing/test_scalar.py @@ -1,4 +1,5 @@ """ test scalar indexing, including at and iat """ +from datetime import datetime, timedelta import numpy as np import pytest @@ -9,61 +10,59 @@ class TestScalar(Base): - def test_at_and_iat_get(self): + @pytest.mark.parametrize("kind", ["series", "frame"]) + def test_at_and_iat_get(self, kind): def _check(f, func, values=False): if f is not None: - indicies = self.generate_indices(f, values) - for i in indicies: + indices = self.generate_indices(f, values) + for i in indices: result = getattr(f, func)[i] expected = self.get_value(func, f, i, values) tm.assert_almost_equal(result, expected) - for kind in self._kinds: + d = getattr(self, kind) - d = getattr(self, kind) + # iat + for f in [d["ints"], d["uints"]]: + _check(f, "iat", values=True) - # iat - for f in [d["ints"], d["uints"]]: - _check(f, "iat", values=True) - - for f in [d["labels"], d["ts"], d["floats"]]: - if f is not None: - msg = "iAt based indexing can only have integer indexers" - with pytest.raises(ValueError, match=msg): - self.check_values(f, "iat") + for f in [d["labels"], d["ts"], d["floats"]]: + if f is not None: + msg = "iAt based indexing can only have integer indexers" + with pytest.raises(ValueError, match=msg): + self.check_values(f, "iat") - # at - for f in [d["ints"], d["uints"], d["labels"], d["ts"], d["floats"]]: - _check(f, "at") + # at + for f in [d["ints"], d["uints"], d["labels"], d["ts"], d["floats"]]: + _check(f, "at") - def test_at_and_iat_set(self): + @pytest.mark.parametrize("kind", ["series", "frame"]) + def test_at_and_iat_set(self, kind): def _check(f, func, values=False): if f is not None: - indicies = self.generate_indices(f, values) - for i in indicies: + indices = self.generate_indices(f, values) + for i in indices: getattr(f, func)[i] = 1 expected = self.get_value(func, f, i, values) tm.assert_almost_equal(expected, 1) - for kind in self._kinds: + d = getattr(self, kind) - d = getattr(self, kind) + # iat + for f in [d["ints"], d["uints"]]: + _check(f, "iat", values=True) - # iat - for f in [d["ints"], d["uints"]]: - _check(f, "iat", values=True) - - for f in [d["labels"], d["ts"], d["floats"]]: - if f is not None: - msg = "iAt based indexing can only have integer indexers" - with pytest.raises(ValueError, match=msg): - _check(f, "iat") + for f in [d["labels"], d["ts"], d["floats"]]: + if f is not None: + msg = "iAt based indexing can only have integer indexers" + with pytest.raises(ValueError, match=msg): + _check(f, "iat") - # at - for f in [d["ints"], d["uints"], d["labels"], d["ts"], d["floats"]]: - _check(f, "at") + # at + for f in [d["ints"], d["uints"], d["labels"], d["ts"], d["floats"]]: + _check(f, "at") class TestScalar2: @@ -139,16 +138,12 @@ def test_series_at_raises_type_error(self): result = ser.loc["a"] assert result == 1 - msg = ( - "cannot do label indexing on Index " - r"with these indexers \[0\] of type int" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): ser.at[0] - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): ser.loc[0] - def test_frame_raises_type_error(self): + def test_frame_raises_key_error(self): # GH#31724 .at should match .loc df = DataFrame({"A": [1, 2, 3]}, index=list("abc")) result = df.at["a", "A"] @@ -156,13 +151,9 @@ def test_frame_raises_type_error(self): result = df.loc["a", "A"] assert result == 1 - msg = ( - "cannot do label indexing on Index " - r"with these indexers \[0\] of type int" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): df.at["a", 0] - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): df.loc["a", 0] def test_series_at_raises_key_error(self): @@ -290,3 +281,24 @@ def test_getitem_zerodim_np_array(self): s = Series([1, 2]) result = s[np.array(0)] assert result == 1 + + +def test_iat_dont_wrap_object_datetimelike(): + # GH#32809 .iat calls go through DataFrame._get_value, should not + # call maybe_box_datetimelike + dti = date_range("2016-01-01", periods=3) + tdi = dti - dti + ser = Series(dti.to_pydatetime(), dtype=object) + ser2 = Series(tdi.to_pytimedelta(), dtype=object) + df = DataFrame({"A": ser, "B": ser2}) + assert (df.dtypes == object).all() + + for result in [df.at[0, "A"], df.iat[0, 0], df.loc[0, "A"], df.iloc[0, 0]]: + assert result is ser[0] + assert isinstance(result, datetime) + assert not isinstance(result, Timestamp) + + for result in [df.at[1, "B"], df.iat[1, 1], df.loc[1, "B"], df.iloc[1, 1]]: + assert result is ser2[1] + assert isinstance(result, timedelta) + assert not isinstance(result, Timedelta) diff --git a/pandas/tests/internals/test_internals.py b/pandas/tests/internals/test_internals.py index aa966caa63238..27b0500983afd 100644 --- a/pandas/tests/internals/test_internals.py +++ b/pandas/tests/internals/test_internals.py @@ -91,9 +91,7 @@ def create_block(typestr, placement, item_shape=None, num_offset=0): elif typestr in ("complex", "c16", "c8"): values = 1.0j * (mat.astype(typestr) + num_offset) elif typestr in ("object", "string", "O"): - values = np.reshape( - ["A{i:d}".format(i=i) for i in mat.ravel() + num_offset], shape - ) + values = np.reshape([f"A{i:d}" for i in mat.ravel() + num_offset], shape) elif typestr in ("b", "bool"): values = np.ones(shape, dtype=np.bool_) elif typestr in ("datetime", "dt", "M8[ns]"): @@ -101,7 +99,7 @@ def create_block(typestr, placement, item_shape=None, num_offset=0): elif typestr.startswith("M8[ns"): # datetime with tz m = re.search(r"M8\[ns,\s*(\w+\/?\w*)\]", typestr) - assert m is not None, "incompatible typestr -> {0}".format(typestr) + assert m is not None, f"incompatible typestr -> {typestr}" tz = m.groups()[0] assert num_items == 1, "must have only 1 num items for a tz-aware" values = DatetimeIndex(np.arange(N) * 1e9, tz=tz) @@ -205,12 +203,6 @@ def create_mgr(descr, item_shape=None): class TestBlock: def setup_method(self, method): - # self.fblock = get_float_ex() # a,c,e - # self.cblock = get_complex_ex() # - # self.oblock = get_obj_ex() - # self.bool_block = get_bool_ex() - # self.int_block = get_int_ex() - self.fblock = create_block("float", [0, 2, 4]) self.cblock = create_block("complex", [7]) self.oblock = create_block("object", [1, 3]) @@ -256,22 +248,11 @@ def test_merge(self): tm.assert_numpy_array_equal(merged.values[[0, 2]], np.array(avals)) tm.assert_numpy_array_equal(merged.values[[1, 3]], np.array(bvals)) - # TODO: merge with mixed type? - def test_copy(self): cop = self.fblock.copy() assert cop is not self.fblock assert_block_equal(self.fblock, cop) - def test_reindex_index(self): - pass - - def test_reindex_cast(self): - pass - - def test_insert(self): - pass - def test_delete(self): newb = self.fblock.copy() newb.delete(0) @@ -302,39 +283,7 @@ def test_delete(self): newb.delete(3) -class TestDatetimeBlock: - def test_can_hold_element(self): - block = create_block("datetime", [0]) - - # We will check that block._can_hold_element iff arr.__setitem__ works - arr = pd.array(block.values.ravel()) - - # coerce None - assert block._can_hold_element(None) - arr[0] = None - assert arr[0] is pd.NaT - - # coerce different types of datetime objects - vals = [np.datetime64("2010-10-10"), datetime(2010, 10, 10)] - for val in vals: - assert block._can_hold_element(val) - arr[0] = val - - val = date(2010, 10, 10) - assert not block._can_hold_element(val) - - msg = ( - "'value' should be a 'Timestamp', 'NaT', " - "or array of those. Got 'date' instead." - ) - with pytest.raises(TypeError, match=msg): - arr[0] = val - - class TestBlockManager: - def test_constructor_corner(self): - pass - def test_attrs(self): mgr = create_mgr("a,b,c: f8-1; d,e,f: f8-2") assert mgr.nblocks == 2 @@ -376,9 +325,6 @@ def test_pickle(self, mgr): mgr2 = tm.round_trip_pickle(mgr) tm.assert_frame_equal(DataFrame(mgr), DataFrame(mgr2)) - # share ref_items - # assert mgr2.blocks[0].ref_items is mgr2.blocks[1].ref_items - # GH2431 assert hasattr(mgr2, "_is_consolidated") assert hasattr(mgr2, "_known_consolidated") @@ -446,18 +392,6 @@ def test_set_change_dtype(self, mgr): mgr2.set("quux", tm.randn(N)) assert mgr2.get("quux").dtype == np.float_ - def test_set_change_dtype_slice(self): # GH8850 - cols = MultiIndex.from_tuples([("1st", "a"), ("2nd", "b"), ("3rd", "c")]) - df = DataFrame([[1.0, 2, 3], [4.0, 5, 6]], columns=cols) - df["2nd"] = df["2nd"] * 2.0 - - blocks = df._to_dict_of_blocks() - assert sorted(blocks.keys()) == ["float64", "int64"] - tm.assert_frame_equal( - blocks["float64"], DataFrame([[1.0, 4.0], [4.0, 10.0]], columns=cols[:2]) - ) - tm.assert_frame_equal(blocks["int64"], DataFrame([[3], [6]], columns=cols[2:])) - def test_copy(self, mgr): cp = mgr.copy(deep=False) for blk, cp_blk in zip(mgr.blocks, cp.blocks): @@ -491,7 +425,7 @@ def test_sparse_mixed(self): assert len(mgr.blocks) == 3 assert isinstance(mgr, BlockManager) - # what to test here? + # TODO: what to test here? def test_as_array_float(self): mgr = create_mgr("c: f4; d: f2; e: f8") @@ -610,9 +544,9 @@ def test_interleave(self): # self for dtype in ["f8", "i8", "object", "bool", "complex", "M8[ns]", "m8[ns]"]: - mgr = create_mgr("a: {0}".format(dtype)) + mgr = create_mgr(f"a: {dtype}") assert mgr.as_array().dtype == dtype - mgr = create_mgr("a: {0}; b: {0}".format(dtype)) + mgr = create_mgr(f"a: {dtype}; b: {dtype}") assert mgr.as_array().dtype == dtype # will be converted according the actual dtype of the underlying @@ -655,22 +589,6 @@ def test_interleave(self): mgr = create_mgr("a: M8[ns]; b: m8[ns]") assert mgr.as_array().dtype == "object" - def test_interleave_non_unique_cols(self): - df = DataFrame( - [[pd.Timestamp("20130101"), 3.5], [pd.Timestamp("20130102"), 4.5]], - columns=["x", "x"], - index=[1, 2], - ) - - df_unique = df.copy() - df_unique.columns = ["x", "y"] - assert df_unique.values.shape == df.values.shape - tm.assert_numpy_array_equal(df_unique.values[0], df.values[0]) - tm.assert_numpy_array_equal(df_unique.values[1], df.values[1]) - - def test_consolidate(self): - pass - def test_consolidate_ordering_issues(self, mgr): mgr.set("f", tm.randn(N)) mgr.set("d", tm.randn(N)) @@ -688,10 +606,6 @@ def test_consolidate_ordering_issues(self, mgr): cons.blocks[0].mgr_locs.as_array, np.arange(len(cons.items), dtype=np.int64) ) - def test_reindex_index(self): - # TODO: should this be pytest.skip? - pass - def test_reindex_items(self): # mgr is not consolidated, f8 & f8-2 blocks mgr = create_mgr("a: f8; b: i8; c: f8; d: i8; e: f8; f: bool; g: f8-2") @@ -772,13 +686,6 @@ def test_get_bool_data(self): def test_unicode_repr_doesnt_raise(self): repr(create_mgr("b,\u05d0: object")) - def test_missing_unicode_key(self): - df = DataFrame({"a": [1]}) - try: - df.loc[:, "\u05d0"] # should not raise UnicodeEncodeError - except KeyError: - pass # this is the expected exception - def test_equals(self): # unique items bm1 = create_mgr("a,b,c: i8-1; d,e,f: i8-2") @@ -789,40 +696,39 @@ def test_equals(self): bm2 = BlockManager(bm1.blocks[::-1], bm1.axes) assert bm1.equals(bm2) - def test_equals_block_order_different_dtypes(self): - # GH 9330 - - mgr_strings = [ + @pytest.mark.parametrize( + "mgr_string", + [ "a:i8;b:f8", # basic case "a:i8;b:f8;c:c8;d:b", # many types "a:i8;e:dt;f:td;g:string", # more types "a:i8;b:category;c:category2;d:category2", # categories "c:sparse;d:sparse_na;b:f8", # sparse - ] - - for mgr_string in mgr_strings: - bm = create_mgr(mgr_string) - block_perms = itertools.permutations(bm.blocks) - for bm_perm in block_perms: - bm_this = BlockManager(bm_perm, bm.axes) - assert bm.equals(bm_this) - assert bm_this.equals(bm) + ], + ) + def test_equals_block_order_different_dtypes(self, mgr_string): + # GH 9330 + bm = create_mgr(mgr_string) + block_perms = itertools.permutations(bm.blocks) + for bm_perm in block_perms: + bm_this = BlockManager(bm_perm, bm.axes) + assert bm.equals(bm_this) + assert bm_this.equals(bm) def test_single_mgr_ctor(self): mgr = create_single_mgr("f8", num_rows=5) assert mgr.as_array().tolist() == [0.0, 1.0, 2.0, 3.0, 4.0] - def test_validate_bool_args(self): - invalid_values = [1, "True", [1, 2, 3], 5.0] + @pytest.mark.parametrize("value", [1, "True", [1, 2, 3], 5.0]) + def test_validate_bool_args(self, value): bm1 = create_mgr("a,b,c: i8-1; d,e,f: i8-2") - for value in invalid_values: - msg = ( - 'For argument "inplace" expected type bool, ' - f"received type {type(value).__name__}." - ) - with pytest.raises(ValueError, match=msg): - bm1.replace_list([1], [2], inplace=value) + msg = ( + 'For argument "inplace" expected type bool, ' + f"received type {type(value).__name__}." + ) + with pytest.raises(ValueError, match=msg): + bm1.replace_list([1], [2], inplace=value) class TestIndexing: @@ -849,9 +755,8 @@ class TestIndexing: create_mgr("a,b: f8; c,d: i8; e,f: f8", item_shape=(N, N)), ] - # MANAGERS = [MANAGERS[6]] - - def test_get_slice(self): + @pytest.mark.parametrize("mgr", MANAGERS) + def test_get_slice(self, mgr): def assert_slice_ok(mgr, axis, slobj): mat = mgr.as_array() @@ -870,35 +775,33 @@ def assert_slice_ok(mgr, axis, slobj): ) tm.assert_index_equal(mgr.axes[axis][slobj], sliced.axes[axis]) - for mgr in self.MANAGERS: - for ax in range(mgr.ndim): - # slice - assert_slice_ok(mgr, ax, slice(None)) - assert_slice_ok(mgr, ax, slice(3)) - assert_slice_ok(mgr, ax, slice(100)) - assert_slice_ok(mgr, ax, slice(1, 4)) - assert_slice_ok(mgr, ax, slice(3, 0, -2)) - - # boolean mask - assert_slice_ok(mgr, ax, np.array([], dtype=np.bool_)) - assert_slice_ok(mgr, ax, np.ones(mgr.shape[ax], dtype=np.bool_)) - assert_slice_ok(mgr, ax, np.zeros(mgr.shape[ax], dtype=np.bool_)) - - if mgr.shape[ax] >= 3: - assert_slice_ok(mgr, ax, np.arange(mgr.shape[ax]) % 3 == 0) - assert_slice_ok( - mgr, ax, np.array([True, True, False], dtype=np.bool_) - ) - - # fancy indexer - assert_slice_ok(mgr, ax, []) - assert_slice_ok(mgr, ax, list(range(mgr.shape[ax]))) - - if mgr.shape[ax] >= 3: - assert_slice_ok(mgr, ax, [0, 1, 2]) - assert_slice_ok(mgr, ax, [-1, -2, -3]) - - def test_take(self): + for ax in range(mgr.ndim): + # slice + assert_slice_ok(mgr, ax, slice(None)) + assert_slice_ok(mgr, ax, slice(3)) + assert_slice_ok(mgr, ax, slice(100)) + assert_slice_ok(mgr, ax, slice(1, 4)) + assert_slice_ok(mgr, ax, slice(3, 0, -2)) + + # boolean mask + assert_slice_ok(mgr, ax, np.array([], dtype=np.bool_)) + assert_slice_ok(mgr, ax, np.ones(mgr.shape[ax], dtype=np.bool_)) + assert_slice_ok(mgr, ax, np.zeros(mgr.shape[ax], dtype=np.bool_)) + + if mgr.shape[ax] >= 3: + assert_slice_ok(mgr, ax, np.arange(mgr.shape[ax]) % 3 == 0) + assert_slice_ok(mgr, ax, np.array([True, True, False], dtype=np.bool_)) + + # fancy indexer + assert_slice_ok(mgr, ax, []) + assert_slice_ok(mgr, ax, list(range(mgr.shape[ax]))) + + if mgr.shape[ax] >= 3: + assert_slice_ok(mgr, ax, [0, 1, 2]) + assert_slice_ok(mgr, ax, [-1, -2, -3]) + + @pytest.mark.parametrize("mgr", MANAGERS) + def test_take(self, mgr): def assert_take_ok(mgr, axis, indexer): mat = mgr.as_array() taken = mgr.take(indexer, axis) @@ -907,18 +810,19 @@ def assert_take_ok(mgr, axis, indexer): ) tm.assert_index_equal(mgr.axes[axis].take(indexer), taken.axes[axis]) - for mgr in self.MANAGERS: - for ax in range(mgr.ndim): - # take/fancy indexer - assert_take_ok(mgr, ax, indexer=[]) - assert_take_ok(mgr, ax, indexer=[0, 0, 0]) - assert_take_ok(mgr, ax, indexer=list(range(mgr.shape[ax]))) + for ax in range(mgr.ndim): + # take/fancy indexer + assert_take_ok(mgr, ax, indexer=[]) + assert_take_ok(mgr, ax, indexer=[0, 0, 0]) + assert_take_ok(mgr, ax, indexer=list(range(mgr.shape[ax]))) - if mgr.shape[ax] >= 3: - assert_take_ok(mgr, ax, indexer=[0, 1, 2]) - assert_take_ok(mgr, ax, indexer=[-1, -2, -3]) + if mgr.shape[ax] >= 3: + assert_take_ok(mgr, ax, indexer=[0, 1, 2]) + assert_take_ok(mgr, ax, indexer=[-1, -2, -3]) - def test_reindex_axis(self): + @pytest.mark.parametrize("mgr", MANAGERS) + @pytest.mark.parametrize("fill_value", [None, np.nan, 100.0]) + def test_reindex_axis(self, fill_value, mgr): def assert_reindex_axis_is_ok(mgr, axis, new_labels, fill_value): mat = mgr.as_array() indexer = mgr.axes[axis].get_indexer_for(new_labels) @@ -931,33 +835,27 @@ def assert_reindex_axis_is_ok(mgr, axis, new_labels, fill_value): ) tm.assert_index_equal(reindexed.axes[axis], new_labels) - for mgr in self.MANAGERS: - for ax in range(mgr.ndim): - for fill_value in (None, np.nan, 100.0): - assert_reindex_axis_is_ok(mgr, ax, pd.Index([]), fill_value) - assert_reindex_axis_is_ok(mgr, ax, mgr.axes[ax], fill_value) - assert_reindex_axis_is_ok( - mgr, ax, mgr.axes[ax][[0, 0, 0]], fill_value - ) - assert_reindex_axis_is_ok( - mgr, ax, pd.Index(["foo", "bar", "baz"]), fill_value - ) - assert_reindex_axis_is_ok( - mgr, ax, pd.Index(["foo", mgr.axes[ax][0], "baz"]), fill_value - ) + for ax in range(mgr.ndim): + assert_reindex_axis_is_ok(mgr, ax, pd.Index([]), fill_value) + assert_reindex_axis_is_ok(mgr, ax, mgr.axes[ax], fill_value) + assert_reindex_axis_is_ok(mgr, ax, mgr.axes[ax][[0, 0, 0]], fill_value) + assert_reindex_axis_is_ok( + mgr, ax, pd.Index(["foo", "bar", "baz"]), fill_value + ) + assert_reindex_axis_is_ok( + mgr, ax, pd.Index(["foo", mgr.axes[ax][0], "baz"]), fill_value + ) - if mgr.shape[ax] >= 3: - assert_reindex_axis_is_ok( - mgr, ax, mgr.axes[ax][:-3], fill_value - ) - assert_reindex_axis_is_ok( - mgr, ax, mgr.axes[ax][-3::-1], fill_value - ) - assert_reindex_axis_is_ok( - mgr, ax, mgr.axes[ax][[0, 1, 2, 0, 1, 2]], fill_value - ) - - def test_reindex_indexer(self): + if mgr.shape[ax] >= 3: + assert_reindex_axis_is_ok(mgr, ax, mgr.axes[ax][:-3], fill_value) + assert_reindex_axis_is_ok(mgr, ax, mgr.axes[ax][-3::-1], fill_value) + assert_reindex_axis_is_ok( + mgr, ax, mgr.axes[ax][[0, 1, 2, 0, 1, 2]], fill_value + ) + + @pytest.mark.parametrize("mgr", MANAGERS) + @pytest.mark.parametrize("fill_value", [None, np.nan, 100.0]) + def test_reindex_indexer(self, fill_value, mgr): def assert_reindex_indexer_is_ok(mgr, axis, new_labels, indexer, fill_value): mat = mgr.as_array() reindexed_mat = algos.take_nd(mat, indexer, axis, fill_value=fill_value) @@ -969,65 +867,42 @@ def assert_reindex_indexer_is_ok(mgr, axis, new_labels, indexer, fill_value): ) tm.assert_index_equal(reindexed.axes[axis], new_labels) - for mgr in self.MANAGERS: - for ax in range(mgr.ndim): - for fill_value in (None, np.nan, 100.0): - assert_reindex_indexer_is_ok(mgr, ax, pd.Index([]), [], fill_value) - assert_reindex_indexer_is_ok( - mgr, ax, mgr.axes[ax], np.arange(mgr.shape[ax]), fill_value - ) - assert_reindex_indexer_is_ok( - mgr, - ax, - pd.Index(["foo"] * mgr.shape[ax]), - np.arange(mgr.shape[ax]), - fill_value, - ) - assert_reindex_indexer_is_ok( - mgr, - ax, - mgr.axes[ax][::-1], - np.arange(mgr.shape[ax]), - fill_value, - ) - assert_reindex_indexer_is_ok( - mgr, - ax, - mgr.axes[ax], - np.arange(mgr.shape[ax])[::-1], - fill_value, - ) - assert_reindex_indexer_is_ok( - mgr, ax, pd.Index(["foo", "bar", "baz"]), [0, 0, 0], fill_value - ) - assert_reindex_indexer_is_ok( - mgr, - ax, - pd.Index(["foo", "bar", "baz"]), - [-1, 0, -1], - fill_value, - ) - assert_reindex_indexer_is_ok( - mgr, - ax, - pd.Index(["foo", mgr.axes[ax][0], "baz"]), - [-1, -1, -1], - fill_value, - ) - - if mgr.shape[ax] >= 3: - assert_reindex_indexer_is_ok( - mgr, - ax, - pd.Index(["foo", "bar", "baz"]), - [0, 1, 2], - fill_value, - ) + for ax in range(mgr.ndim): + assert_reindex_indexer_is_ok(mgr, ax, pd.Index([]), [], fill_value) + assert_reindex_indexer_is_ok( + mgr, ax, mgr.axes[ax], np.arange(mgr.shape[ax]), fill_value + ) + assert_reindex_indexer_is_ok( + mgr, + ax, + pd.Index(["foo"] * mgr.shape[ax]), + np.arange(mgr.shape[ax]), + fill_value, + ) + assert_reindex_indexer_is_ok( + mgr, ax, mgr.axes[ax][::-1], np.arange(mgr.shape[ax]), fill_value, + ) + assert_reindex_indexer_is_ok( + mgr, ax, mgr.axes[ax], np.arange(mgr.shape[ax])[::-1], fill_value, + ) + assert_reindex_indexer_is_ok( + mgr, ax, pd.Index(["foo", "bar", "baz"]), [0, 0, 0], fill_value + ) + assert_reindex_indexer_is_ok( + mgr, ax, pd.Index(["foo", "bar", "baz"]), [-1, 0, -1], fill_value, + ) + assert_reindex_indexer_is_ok( + mgr, + ax, + pd.Index(["foo", mgr.axes[ax][0], "baz"]), + [-1, -1, -1], + fill_value, + ) - # test_get_slice(slice_like, axis) - # take(indexer, axis) - # reindex_axis(new_labels, axis) - # reindex_indexer(new_labels, indexer, axis) + if mgr.shape[ax] >= 3: + assert_reindex_indexer_is_ok( + mgr, ax, pd.Index(["foo", "bar", "baz"]), [0, 1, 2], fill_value, + ) class TestBlockPlacement: @@ -1164,7 +1039,7 @@ def __array__(self): return np.array(self.value, dtype=self.dtype) def __str__(self) -> str: - return "DummyElement({}, {})".format(self.value, self.dtype) + return f"DummyElement({self.value}, {self.dtype})" def __repr__(self) -> str: return str(self) @@ -1181,6 +1056,33 @@ def any(self, axis=None): class TestCanHoldElement: + def test_datetime_block_can_hold_element(self): + block = create_block("datetime", [0]) + + # We will check that block._can_hold_element iff arr.__setitem__ works + arr = pd.array(block.values.ravel()) + + # coerce None + assert block._can_hold_element(None) + arr[0] = None + assert arr[0] is pd.NaT + + # coerce different types of datetime objects + vals = [np.datetime64("2010-10-10"), datetime(2010, 10, 10)] + for val in vals: + assert block._can_hold_element(val) + arr[0] = val + + val = date(2010, 10, 10) + assert not block._can_hold_element(val) + + msg = ( + "'value' should be a 'Timestamp', 'NaT', " + "or array of those. Got 'date' instead." + ) + with pytest.raises(TypeError, match=msg): + arr[0] = val + @pytest.mark.parametrize( "value, dtype", [ @@ -1310,3 +1212,37 @@ def test_dataframe_not_equal(): df1 = pd.DataFrame({"a": [1, 2], "b": ["s", "d"]}) df2 = pd.DataFrame({"a": ["s", "d"], "b": [1, 2]}) assert df1.equals(df2) is False + + +def test_missing_unicode_key(): + df = DataFrame({"a": [1]}) + with pytest.raises(KeyError, match="\u05d0"): + df.loc[:, "\u05d0"] # should not raise UnicodeEncodeError + + +def test_set_change_dtype_slice(): + # GH#8850 + cols = MultiIndex.from_tuples([("1st", "a"), ("2nd", "b"), ("3rd", "c")]) + df = DataFrame([[1.0, 2, 3], [4.0, 5, 6]], columns=cols) + df["2nd"] = df["2nd"] * 2.0 + + blocks = df._to_dict_of_blocks() + assert sorted(blocks.keys()) == ["float64", "int64"] + tm.assert_frame_equal( + blocks["float64"], DataFrame([[1.0, 4.0], [4.0, 10.0]], columns=cols[:2]) + ) + tm.assert_frame_equal(blocks["int64"], DataFrame([[3], [6]], columns=cols[2:])) + + +def test_interleave_non_unique_cols(): + df = DataFrame( + [[pd.Timestamp("20130101"), 3.5], [pd.Timestamp("20130102"), 4.5]], + columns=["x", "x"], + index=[1, 2], + ) + + df_unique = df.copy() + df_unique.columns = ["x", "y"] + assert df_unique.values.shape == df.values.shape + tm.assert_numpy_array_equal(df_unique.values[0], df.values[0]) + tm.assert_numpy_array_equal(df_unique.values[1], df.values[1]) diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py index 7810778602e12..fe71ca77a7dda 100644 --- a/pandas/tests/io/conftest.py +++ b/pandas/tests/io/conftest.py @@ -27,7 +27,8 @@ def salaries_table(datapath): @pytest.fixture def s3_resource(tips_file, jsonl_file): - """Fixture for mocking S3 interaction. + """ + Fixture for mocking S3 interaction. The primary bucket name is "pandas-test". The following datasets are loaded. diff --git a/pandas/tests/io/data/legacy_hdf/legacy_table_fixed_datetime_py2.h5 b/pandas/tests/io/data/legacy_hdf/legacy_table_fixed_datetime_py2.h5 new file mode 100644 index 0000000000000..18cfae15a3a78 Binary files /dev/null and b/pandas/tests/io/data/legacy_hdf/legacy_table_fixed_datetime_py2.h5 differ diff --git a/pandas/tests/io/data/pickle/test_mi_py27.pkl b/pandas/tests/io/data/pickle/test_mi_py27.pkl new file mode 100644 index 0000000000000..89021dd828108 Binary files /dev/null and b/pandas/tests/io/data/pickle/test_mi_py27.pkl differ diff --git a/pandas/tests/io/excel/test_readers.py b/pandas/tests/io/excel/test_readers.py index 8d00ef1b7fe3e..a59b409809eed 100644 --- a/pandas/tests/io/excel/test_readers.py +++ b/pandas/tests/io/excel/test_readers.py @@ -596,7 +596,8 @@ def test_read_from_file_url(self, read_ext, datapath): # fails on some systems import platform - pytest.skip("failing on {}".format(" ".join(platform.uname()).strip())) + platform_info = " ".join(platform.uname()).strip() + pytest.skip(f"failing on {platform_info}") tm.assert_frame_equal(url_table, local_table) @@ -957,7 +958,7 @@ def test_excel_passes_na_filter(self, read_ext, na_filter): def test_unexpected_kwargs_raises(self, read_ext, arg): # gh-17964 kwarg = {arg: "Sheet1"} - msg = r"unexpected keyword argument `{}`".format(arg) + msg = fr"unexpected keyword argument `{arg}`" with pd.ExcelFile("test1" + read_ext) as excel: with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 88f4c3736bc0d..31b033f381f0c 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -45,10 +45,7 @@ def style(df): def assert_equal_style(cell1, cell2, engine): if engine in ["xlsxwriter", "openpyxl"]: pytest.xfail( - reason=( - "GH25351: failing on some attribute " - "comparisons in {}".format(engine) - ) + reason=(f"GH25351: failing on some attribute comparisons in {engine}") ) # XXX: should find a better way to check equality assert cell1.alignment.__dict__ == cell2.alignment.__dict__ @@ -108,7 +105,7 @@ def custom_converter(css): for col1, col2 in zip(wb["frame"].columns, wb["styled"].columns): assert len(col1) == len(col2) for cell1, cell2 in zip(col1, col2): - ref = "{cell2.column}{cell2.row:d}".format(cell2=cell2) + ref = f"{cell2.column}{cell2.row:d}" # XXX: this isn't as strong a test as ideal; we should # confirm that differences are exclusive if ref == "B2": @@ -156,7 +153,7 @@ def custom_converter(css): for col1, col2 in zip(wb["frame"].columns, wb["custom"].columns): assert len(col1) == len(col2) for cell1, cell2 in zip(col1, col2): - ref = "{cell2.column}{cell2.row:d}".format(cell2=cell2) + ref = f"{cell2.column}{cell2.row:d}" if ref in ("B2", "C3", "D4", "B5", "C6", "D7", "B8", "B9"): assert not cell1.font.bold assert cell2.font.bold diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index 91665a24fc4c5..506d223dbedb4 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -41,7 +41,7 @@ def set_engine(engine, ext): which engine should be used to write Excel files. After executing the test it rolls back said change to the global option. """ - option_name = "io.excel.{ext}.writer".format(ext=ext.strip(".")) + option_name = f"io.excel.{ext.strip('.')}.writer" prev_engine = get_option(option_name) set_option(option_name, engine) yield @@ -1206,7 +1206,7 @@ def test_path_path_lib(self, engine, ext): writer = partial(df.to_excel, engine=engine) reader = partial(pd.read_excel, index_col=0) - result = tm.round_trip_pathlib(writer, reader, path="foo.{ext}".format(ext=ext)) + result = tm.round_trip_pathlib(writer, reader, path=f"foo.{ext}") tm.assert_frame_equal(result, df) def test_path_local_path(self, engine, ext): @@ -1214,7 +1214,7 @@ def test_path_local_path(self, engine, ext): writer = partial(df.to_excel, engine=engine) reader = partial(pd.read_excel, index_col=0) - result = tm.round_trip_pathlib(writer, reader, path="foo.{ext}".format(ext=ext)) + result = tm.round_trip_pathlib(writer, reader, path=f"foo.{ext}") tm.assert_frame_equal(result, df) def test_merged_cell_custom_objects(self, merge_cells, path): diff --git a/pandas/tests/io/excel/test_xlrd.py b/pandas/tests/io/excel/test_xlrd.py index cc7e2311f362a..d456afe4ed351 100644 --- a/pandas/tests/io/excel/test_xlrd.py +++ b/pandas/tests/io/excel/test_xlrd.py @@ -37,7 +37,7 @@ def test_read_xlrd_book(read_ext, frame): # TODO: test for openpyxl as well def test_excel_table_sheet_by_index(datapath, read_ext): - path = datapath("io", "data", "excel", "test1{}".format(read_ext)) + path = datapath("io", "data", "excel", f"test1{read_ext}") with pd.ExcelFile(path) as excel: with pytest.raises(xlrd.XLRDError): pd.read_excel(excel, "asdf") diff --git a/pandas/tests/io/formats/test_console.py b/pandas/tests/io/formats/test_console.py index e56d14885f11e..b57a2393461a2 100644 --- a/pandas/tests/io/formats/test_console.py +++ b/pandas/tests/io/formats/test_console.py @@ -34,8 +34,8 @@ def test_detect_console_encoding_from_stdout_stdin(monkeypatch, empty, filled): # they have values filled. # GH 21552 with monkeypatch.context() as context: - context.setattr("sys.{}".format(empty), MockEncoding("")) - context.setattr("sys.{}".format(filled), MockEncoding(filled)) + context.setattr(f"sys.{empty}", MockEncoding("")) + context.setattr(f"sys.{filled}", MockEncoding(filled)) assert detect_console_encoding() == filled diff --git a/pandas/tests/io/formats/test_info.py b/pandas/tests/io/formats/test_info.py new file mode 100644 index 0000000000000..877bd1650ae60 --- /dev/null +++ b/pandas/tests/io/formats/test_info.py @@ -0,0 +1,405 @@ +from io import StringIO +import re +from string import ascii_uppercase as uppercase +import sys +import textwrap + +import numpy as np +import pytest + +from pandas.compat import PYPY + +from pandas import ( + CategoricalIndex, + DataFrame, + MultiIndex, + Series, + date_range, + option_context, + reset_option, + set_option, +) +import pandas._testing as tm + + +@pytest.fixture +def datetime_frame(): + """ + Fixture for DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D'] + + A B C D + 2000-01-03 -1.122153 0.468535 0.122226 1.693711 + 2000-01-04 0.189378 0.486100 0.007864 -1.216052 + 2000-01-05 0.041401 -0.835752 -0.035279 -0.414357 + 2000-01-06 0.430050 0.894352 0.090719 0.036939 + 2000-01-07 -0.620982 -0.668211 -0.706153 1.466335 + 2000-01-10 -0.752633 0.328434 -0.815325 0.699674 + 2000-01-11 -2.236969 0.615737 -0.829076 -1.196106 + ... ... ... ... ... + 2000-02-03 1.642618 -0.579288 0.046005 1.385249 + 2000-02-04 -0.544873 -1.160962 -0.284071 -1.418351 + 2000-02-07 -2.656149 -0.601387 1.410148 0.444150 + 2000-02-08 -1.201881 -1.289040 0.772992 -1.445300 + 2000-02-09 1.377373 0.398619 1.008453 -0.928207 + 2000-02-10 0.473194 -0.636677 0.984058 0.511519 + 2000-02-11 -0.965556 0.408313 -1.312844 -0.381948 + + [30 rows x 4 columns] + """ + return DataFrame(tm.getTimeSeriesData()) + + +def test_info_categorical_column(): + + # make sure it works + n = 2500 + df = DataFrame({"int64": np.random.randint(100, size=n)}) + df["category"] = Series( + np.array(list("abcdefghij")).take(np.random.randint(0, 10, size=n)) + ).astype("category") + df.isna() + buf = StringIO() + df.info(buf=buf) + + df2 = df[df["category"] == "d"] + buf = StringIO() + df2.info(buf=buf) + + +def test_info(float_frame, datetime_frame): + io = StringIO() + float_frame.info(buf=io) + datetime_frame.info(buf=io) + + frame = DataFrame(np.random.randn(5, 3)) + + frame.info() + frame.info(verbose=False) + + +def test_info_verbose(): + buf = StringIO() + size = 1001 + start = 5 + frame = DataFrame(np.random.randn(3, size)) + frame.info(verbose=True, buf=buf) + + res = buf.getvalue() + header = " # Column Dtype \n--- ------ ----- " + assert header in res + + frame.info(verbose=True, buf=buf) + buf.seek(0) + lines = buf.readlines() + assert len(lines) > 0 + + for i, line in enumerate(lines): + if i >= start and i < start + size: + line_nr = f" {i - start} " + assert line.startswith(line_nr) + + +def test_info_memory(): + # https://github.com/pandas-dev/pandas/issues/21056 + df = DataFrame({"a": Series([1, 2], dtype="i8")}) + buf = StringIO() + df.info(buf=buf) + result = buf.getvalue() + bytes = float(df.memory_usage().sum()) + expected = textwrap.dedent( + f"""\ + + RangeIndex: 2 entries, 0 to 1 + Data columns (total 1 columns): + # Column Non-Null Count Dtype + --- ------ -------------- ----- + 0 a 2 non-null int64 + dtypes: int64(1) + memory usage: {bytes} bytes + """ + ) + assert result == expected + + +def test_info_wide(): + io = StringIO() + df = DataFrame(np.random.randn(5, 101)) + df.info(buf=io) + + io = StringIO() + df.info(buf=io, max_cols=101) + rs = io.getvalue() + assert len(rs.splitlines()) > 100 + xp = rs + + set_option("display.max_info_columns", 101) + io = StringIO() + df.info(buf=io) + assert rs == xp + reset_option("display.max_info_columns") + + +def test_info_duplicate_columns(): + io = StringIO() + + # it works! + frame = DataFrame(np.random.randn(1500, 4), columns=["a", "a", "b", "b"]) + frame.info(buf=io) + + +def test_info_duplicate_columns_shows_correct_dtypes(): + # GH11761 + io = StringIO() + + frame = DataFrame([[1, 2.0]], columns=["a", "a"]) + frame.info(buf=io) + io.seek(0) + lines = io.readlines() + assert " 0 a 1 non-null int64 \n" == lines[5] + assert " 1 a 1 non-null float64\n" == lines[6] + + +def test_info_shows_column_dtypes(): + dtypes = [ + "int64", + "float64", + "datetime64[ns]", + "timedelta64[ns]", + "complex128", + "object", + "bool", + ] + data = {} + n = 10 + for i, dtype in enumerate(dtypes): + data[i] = np.random.randint(2, size=n).astype(dtype) + df = DataFrame(data) + buf = StringIO() + df.info(buf=buf) + res = buf.getvalue() + header = ( + " # Column Non-Null Count Dtype \n" + "--- ------ -------------- ----- " + ) + assert header in res + for i, dtype in enumerate(dtypes): + name = f" {i:d} {i:d} {n:d} non-null {dtype}" + assert name in res + + +def test_info_max_cols(): + df = DataFrame(np.random.randn(10, 5)) + for len_, verbose in [(5, None), (5, False), (12, True)]: + # For verbose always ^ setting ^ summarize ^ full output + with option_context("max_info_columns", 4): + buf = StringIO() + df.info(buf=buf, verbose=verbose) + res = buf.getvalue() + assert len(res.strip().split("\n")) == len_ + + for len_, verbose in [(12, None), (5, False), (12, True)]: + + # max_cols not exceeded + with option_context("max_info_columns", 5): + buf = StringIO() + df.info(buf=buf, verbose=verbose) + res = buf.getvalue() + assert len(res.strip().split("\n")) == len_ + + for len_, max_cols in [(12, 5), (5, 4)]: + # setting truncates + with option_context("max_info_columns", 4): + buf = StringIO() + df.info(buf=buf, max_cols=max_cols) + res = buf.getvalue() + assert len(res.strip().split("\n")) == len_ + + # setting wouldn't truncate + with option_context("max_info_columns", 5): + buf = StringIO() + df.info(buf=buf, max_cols=max_cols) + res = buf.getvalue() + assert len(res.strip().split("\n")) == len_ + + +def test_info_memory_usage(): + # Ensure memory usage is displayed, when asserted, on the last line + dtypes = [ + "int64", + "float64", + "datetime64[ns]", + "timedelta64[ns]", + "complex128", + "object", + "bool", + ] + data = {} + n = 10 + for i, dtype in enumerate(dtypes): + data[i] = np.random.randint(2, size=n).astype(dtype) + df = DataFrame(data) + buf = StringIO() + + # display memory usage case + df.info(buf=buf, memory_usage=True) + res = buf.getvalue().splitlines() + assert "memory usage: " in res[-1] + + # do not display memory usage case + df.info(buf=buf, memory_usage=False) + res = buf.getvalue().splitlines() + assert "memory usage: " not in res[-1] + + df.info(buf=buf, memory_usage=True) + res = buf.getvalue().splitlines() + + # memory usage is a lower bound, so print it as XYZ+ MB + assert re.match(r"memory usage: [^+]+\+", res[-1]) + + df.iloc[:, :5].info(buf=buf, memory_usage=True) + res = buf.getvalue().splitlines() + + # excluded column with object dtype, so estimate is accurate + assert not re.match(r"memory usage: [^+]+\+", res[-1]) + + # Test a DataFrame with duplicate columns + dtypes = ["int64", "int64", "int64", "float64"] + data = {} + n = 100 + for i, dtype in enumerate(dtypes): + data[i] = np.random.randint(2, size=n).astype(dtype) + df = DataFrame(data) + df.columns = dtypes + + df_with_object_index = DataFrame({"a": [1]}, index=["foo"]) + df_with_object_index.info(buf=buf, memory_usage=True) + res = buf.getvalue().splitlines() + assert re.match(r"memory usage: [^+]+\+", res[-1]) + + df_with_object_index.info(buf=buf, memory_usage="deep") + res = buf.getvalue().splitlines() + assert re.match(r"memory usage: [^+]+$", res[-1]) + + # Ensure df size is as expected + # (cols * rows * bytes) + index size + df_size = df.memory_usage().sum() + exp_size = len(dtypes) * n * 8 + df.index.nbytes + assert df_size == exp_size + + # Ensure number of cols in memory_usage is the same as df + size_df = np.size(df.columns.values) + 1 # index=True; default + assert size_df == np.size(df.memory_usage()) + + # assert deep works only on object + assert df.memory_usage().sum() == df.memory_usage(deep=True).sum() + + # test for validity + DataFrame(1, index=["a"], columns=["A"]).memory_usage(index=True) + DataFrame(1, index=["a"], columns=["A"]).index.nbytes + df = DataFrame( + data=1, index=MultiIndex.from_product([["a"], range(1000)]), columns=["A"], + ) + df.index.nbytes + df.memory_usage(index=True) + df.index.values.nbytes + + mem = df.memory_usage(deep=True).sum() + assert mem > 0 + + +@pytest.mark.skipif(PYPY, reason="on PyPy deep=True doesn't change result") +def test_info_memory_usage_deep_not_pypy(): + df_with_object_index = DataFrame({"a": [1]}, index=["foo"]) + assert ( + df_with_object_index.memory_usage(index=True, deep=True).sum() + > df_with_object_index.memory_usage(index=True).sum() + ) + + df_object = DataFrame({"a": ["a"]}) + assert df_object.memory_usage(deep=True).sum() > df_object.memory_usage().sum() + + +@pytest.mark.skipif(not PYPY, reason="on PyPy deep=True does not change result") +def test_info_memory_usage_deep_pypy(): + df_with_object_index = DataFrame({"a": [1]}, index=["foo"]) + assert ( + df_with_object_index.memory_usage(index=True, deep=True).sum() + == df_with_object_index.memory_usage(index=True).sum() + ) + + df_object = DataFrame({"a": ["a"]}) + assert df_object.memory_usage(deep=True).sum() == df_object.memory_usage().sum() + + +@pytest.mark.skipif(PYPY, reason="PyPy getsizeof() fails by design") +def test_usage_via_getsizeof(): + df = DataFrame( + data=1, index=MultiIndex.from_product([["a"], range(1000)]), columns=["A"], + ) + mem = df.memory_usage(deep=True).sum() + # sys.getsizeof will call the .memory_usage with + # deep=True, and add on some GC overhead + diff = mem - sys.getsizeof(df) + assert abs(diff) < 100 + + +def test_info_memory_usage_qualified(): + + buf = StringIO() + df = DataFrame(1, columns=list("ab"), index=[1, 2, 3]) + df.info(buf=buf) + assert "+" not in buf.getvalue() + + buf = StringIO() + df = DataFrame(1, columns=list("ab"), index=list("ABC")) + df.info(buf=buf) + assert "+" in buf.getvalue() + + buf = StringIO() + df = DataFrame( + 1, columns=list("ab"), index=MultiIndex.from_product([range(3), range(3)]), + ) + df.info(buf=buf) + assert "+" not in buf.getvalue() + + buf = StringIO() + df = DataFrame( + 1, + columns=list("ab"), + index=MultiIndex.from_product([range(3), ["foo", "bar"]]), + ) + df.info(buf=buf) + assert "+" in buf.getvalue() + + +def test_info_memory_usage_bug_on_multiindex(): + # GH 14308 + # memory usage introspection should not materialize .values + + def memory_usage(f): + return f.memory_usage(deep=True).sum() + + N = 100 + M = len(uppercase) + index = MultiIndex.from_product( + [list(uppercase), date_range("20160101", periods=N)], names=["id", "date"], + ) + df = DataFrame({"value": np.random.randn(N * M)}, index=index) + + unstacked = df.unstack("id") + assert df.values.nbytes == unstacked.values.nbytes + assert memory_usage(df) > memory_usage(unstacked) + + # high upper bound + assert memory_usage(unstacked) - memory_usage(df) < 2000 + + +def test_info_categorical(): + # GH14298 + idx = CategoricalIndex(["a", "b"]) + df = DataFrame(np.zeros((2, 2)), index=idx, columns=idx) + + buf = StringIO() + df.info(buf=buf) diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py index d3f044a42eb28..9a14022d6f776 100644 --- a/pandas/tests/io/formats/test_to_html.py +++ b/pandas/tests/io/formats/test_to_html.py @@ -300,7 +300,7 @@ def test_to_html_border(option, result, expected): else: with option_context("display.html.border", option): result = result(df) - expected = 'border="{}"'.format(expected) + expected = f'border="{expected}"' assert expected in result @@ -318,7 +318,7 @@ def test_to_html(biggie_df_fixture): assert isinstance(s, str) df.to_html(columns=["B", "A"], col_space=17) - df.to_html(columns=["B", "A"], formatters={"A": lambda x: "{x:.1f}".format(x=x)}) + df.to_html(columns=["B", "A"], formatters={"A": lambda x: f"{x:.1f}"}) df.to_html(columns=["B", "A"], float_format=str) df.to_html(columns=["B", "A"], col_space=12, float_format=str) @@ -745,7 +745,7 @@ def test_to_html_with_col_space_units(unit): if isinstance(unit, int): unit = str(unit) + "px" for h in hdrs: - expected = ''.format(unit=unit) + expected = f'' assert expected in h diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index bd681032f155d..c2fbc59b8f482 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -117,10 +117,10 @@ def test_to_latex_with_formatters(self): formatters = { "datetime64": lambda x: x.strftime("%Y-%m"), - "float": lambda x: "[{x: 4.1f}]".format(x=x), - "int": lambda x: "0x{x:x}".format(x=x), - "object": lambda x: "-{x!s}-".format(x=x), - "__index__": lambda x: "index: {x}".format(x=x), + "float": lambda x: f"[{x: 4.1f}]", + "int": lambda x: f"0x{x:x}", + "object": lambda x: f"-{x!s}-", + "__index__": lambda x: f"index: {x}", } result = df.to_latex(formatters=dict(formatters)) @@ -744,9 +744,7 @@ def test_to_latex_multiindex_names(self, name0, name1, axes): idx_names = tuple(n or "{}" for n in names) idx_names_row = ( - "{idx_names[0]} & {idx_names[1]} & & & & \\\\\n".format( - idx_names=idx_names - ) + f"{idx_names[0]} & {idx_names[1]} & & & & \\\\\n" if (0 in axes and any(names)) else "" ) diff --git a/pandas/tests/io/generate_legacy_storage_files.py b/pandas/tests/io/generate_legacy_storage_files.py index 67b767a337a89..ca853ba5f00f5 100755 --- a/pandas/tests/io/generate_legacy_storage_files.py +++ b/pandas/tests/io/generate_legacy_storage_files.py @@ -136,7 +136,6 @@ def _create_sp_frame(): def create_data(): """ create the pickle data """ - data = { "A": [0.0, 1.0, 2.0, 3.0, np.nan], "B": [0, 1, 0, 1, 0], @@ -324,17 +323,17 @@ def write_legacy_pickles(output_dir): "This script generates a storage file for the current arch, system, " "and python version" ) - print(" pandas version: {0}".format(version)) - print(" output dir : {0}".format(output_dir)) + print(f" pandas version: {version}") + print(f" output dir : {output_dir}") print(" storage format: pickle") - pth = "{0}.pickle".format(platform_name()) + pth = f"{platform_name()}.pickle" fh = open(os.path.join(output_dir, pth), "wb") pickle.dump(create_pickle_data(), fh, pickle.HIGHEST_PROTOCOL) fh.close() - print("created pickle file: {pth}".format(pth=pth)) + print(f"created pickle file: {pth}") def write_legacy_file(): diff --git a/pandas/tests/io/json/test_ujson.py b/pandas/tests/io/json/test_ujson.py index bedd60084124c..e966db7a1cc71 100644 --- a/pandas/tests/io/json/test_ujson.py +++ b/pandas/tests/io/json/test_ujson.py @@ -33,7 +33,6 @@ def _clean_dict(d): ------- cleaned_dict : dict """ - return {str(k): v for k, v in d.items()} diff --git a/pandas/tests/io/parser/test_c_parser_only.py b/pandas/tests/io/parser/test_c_parser_only.py index 1737f14e7adf9..5bbabc8e18c47 100644 --- a/pandas/tests/io/parser/test_c_parser_only.py +++ b/pandas/tests/io/parser/test_c_parser_only.py @@ -158,7 +158,7 @@ def test_precise_conversion(c_parser_only): # test numbers between 1 and 2 for num in np.linspace(1.0, 2.0, num=500): # 25 decimal digits of precision - text = "a\n{0:.25}".format(num) + text = f"a\n{num:.25}" normal_val = float(parser.read_csv(StringIO(text))["a"][0]) precise_val = float( @@ -170,7 +170,7 @@ def test_precise_conversion(c_parser_only): actual_val = Decimal(text[2:]) def error(val): - return abs(Decimal("{0:.100}".format(val)) - actual_val) + return abs(Decimal(f"{val:.100}") - actual_val) normal_errors.append(error(normal_val)) precise_errors.append(error(precise_val)) @@ -299,9 +299,7 @@ def test_grow_boundary_at_cap(c_parser_only): def test_empty_header_read(count): s = StringIO("," * count) - expected = DataFrame( - columns=["Unnamed: {i}".format(i=i) for i in range(count + 1)] - ) + expected = DataFrame(columns=[f"Unnamed: {i}" for i in range(count + 1)]) df = parser.read_csv(s) tm.assert_frame_equal(df, expected) @@ -489,7 +487,7 @@ def test_comment_whitespace_delimited(c_parser_only, capsys): captured = capsys.readouterr() # skipped lines 2, 3, 4, 9 for line_num in (2, 3, 4, 9): - assert "Skipping line {}".format(line_num) in captured.err + assert f"Skipping line {line_num}" in captured.err expected = DataFrame([[1, 2], [5, 2], [6, 2], [7, np.nan], [8, np.nan]]) tm.assert_frame_equal(df, expected) diff --git a/pandas/tests/io/parser/test_common.py b/pandas/tests/io/parser/test_common.py index c19056d434ec3..b3aa1aa14a509 100644 --- a/pandas/tests/io/parser/test_common.py +++ b/pandas/tests/io/parser/test_common.py @@ -957,7 +957,7 @@ def test_nonexistent_path(all_parsers): # gh-14086: raise more helpful FileNotFoundError # GH#29233 "File foo" instead of "File b'foo'" parser = all_parsers - path = "{}.csv".format(tm.rands(10)) + path = f"{tm.rands(10)}.csv" msg = f"File {path} does not exist" if parser.engine == "c" else r"\[Errno 2\]" with pytest.raises(FileNotFoundError, match=msg) as e: @@ -1872,7 +1872,7 @@ def test_internal_eof_byte_to_file(all_parsers): parser = all_parsers data = b'c1,c2\r\n"test \x1a test", test\r\n' expected = DataFrame([["test \x1a test", " test"]], columns=["c1", "c2"]) - path = "__{}__.csv".format(tm.rands(10)) + path = f"__{tm.rands(10)}__.csv" with tm.ensure_clean(path) as path: with open(path, "wb") as f: diff --git a/pandas/tests/io/parser/test_compression.py b/pandas/tests/io/parser/test_compression.py index dc03370daa1e2..b773664adda72 100644 --- a/pandas/tests/io/parser/test_compression.py +++ b/pandas/tests/io/parser/test_compression.py @@ -145,7 +145,7 @@ def test_invalid_compression(all_parsers, invalid_compression): parser = all_parsers compress_kwargs = dict(compression=invalid_compression) - msg = "Unrecognized compression type: {compression}".format(**compress_kwargs) + msg = f"Unrecognized compression type: {invalid_compression}" with pytest.raises(ValueError, match=msg): parser.read_csv("test_file.zip", **compress_kwargs) diff --git a/pandas/tests/io/parser/test_encoding.py b/pandas/tests/io/parser/test_encoding.py index 13f72a0414bac..3661e4e056db2 100644 --- a/pandas/tests/io/parser/test_encoding.py +++ b/pandas/tests/io/parser/test_encoding.py @@ -45,7 +45,7 @@ def test_utf16_bom_skiprows(all_parsers, sep, encoding): 4,5,6""".replace( ",", sep ) - path = "__{}__.csv".format(tm.rands(10)) + path = f"__{tm.rands(10)}__.csv" kwargs = dict(sep=sep, skiprows=2) utf8 = "utf-8" diff --git a/pandas/tests/io/parser/test_multi_thread.py b/pandas/tests/io/parser/test_multi_thread.py index 64ccaf60ec230..458ff4da55ed3 100644 --- a/pandas/tests/io/parser/test_multi_thread.py +++ b/pandas/tests/io/parser/test_multi_thread.py @@ -41,9 +41,7 @@ def test_multi_thread_string_io_read_csv(all_parsers): num_files = 100 bytes_to_df = [ - "\n".join( - ["{i:d},{i:d},{i:d}".format(i=i) for i in range(max_row_range)] - ).encode() + "\n".join([f"{i:d},{i:d},{i:d}" for i in range(max_row_range)]).encode() for _ in range(num_files) ] files = [BytesIO(b) for b in bytes_to_df] diff --git a/pandas/tests/io/parser/test_na_values.py b/pandas/tests/io/parser/test_na_values.py index f9a083d7f5d22..9f86bbd65640e 100644 --- a/pandas/tests/io/parser/test_na_values.py +++ b/pandas/tests/io/parser/test_na_values.py @@ -111,10 +111,11 @@ def f(i, v): elif i > 0: buf = "".join([","] * i) - buf = "{0}{1}".format(buf, v) + buf = f"{buf}{v}" if i < nv - 1: - buf = "{0}{1}".format(buf, "".join([","] * (nv - i - 1))) + joined = "".join([","] * (nv - i - 1)) + buf = f"{buf}{joined}" return buf diff --git a/pandas/tests/io/parser/test_parse_dates.py b/pandas/tests/io/parser/test_parse_dates.py index b01b22e811ee3..6f7a1d3d5e351 100644 --- a/pandas/tests/io/parser/test_parse_dates.py +++ b/pandas/tests/io/parser/test_parse_dates.py @@ -1101,7 +1101,7 @@ def test_bad_date_parse(all_parsers, cache_dates, value): # if we have an invalid date make sure that we handle this with # and w/o the cache properly parser = all_parsers - s = StringIO(("{value},\n".format(value=value)) * 50000) + s = StringIO((f"{value},\n") * 50000) parser.read_csv( s, @@ -1516,3 +1516,33 @@ def test_hypothesis_delimited_date(date_format, dayfirst, delimiter, test_dateti assert except_out_dateutil == except_in_dateutil assert result == expected + + +@pytest.mark.parametrize( + "names, usecols, parse_dates, missing_cols", + [ + (None, ["val"], ["date", "time"], "date, time"), + (None, ["val"], [0, "time"], "time"), + (None, ["val"], [["date", "time"]], "date, time"), + (None, ["val"], [[0, "time"]], "time"), + (None, ["val"], {"date": [0, "time"]}, "time"), + (None, ["val"], {"date": ["date", "time"]}, "date, time"), + (None, ["val"], [["date", "time"], "date"], "date, time"), + (["date1", "time1", "temperature"], None, ["date", "time"], "date, time"), + ( + ["date1", "time1", "temperature"], + ["date1", "temperature"], + ["date1", "time"], + "time", + ), + ], +) +def test_missing_column(all_parsers, names, usecols, parse_dates, missing_cols): + """GH31251 column names provided in parse_dates could be missing.""" + parser = all_parsers + content = StringIO("date,time,val\n2020-01-31,04:20:32,32\n") + msg = f"Missing column provided to 'parse_dates': '{missing_cols}'" + with pytest.raises(ValueError, match=msg): + parser.read_csv( + content, sep=",", names=names, usecols=usecols, parse_dates=parse_dates, + ) diff --git a/pandas/tests/io/parser/test_read_fwf.py b/pandas/tests/io/parser/test_read_fwf.py index 27aef2376e87d..e982667f06f31 100644 --- a/pandas/tests/io/parser/test_read_fwf.py +++ b/pandas/tests/io/parser/test_read_fwf.py @@ -260,7 +260,7 @@ def test_fwf_regression(): # Turns out "T060" is parsable as a datetime slice! tz_list = [1, 10, 20, 30, 60, 80, 100] widths = [16] + [8] * len(tz_list) - names = ["SST"] + ["T{z:03d}".format(z=z) for z in tz_list[1:]] + names = ["SST"] + [f"T{z:03d}" for z in tz_list[1:]] data = """ 2009164202000 9.5403 9.4105 8.6571 7.8372 6.0612 5.8843 5.5192 2009164203000 9.5435 9.2010 8.6167 7.8176 6.0804 5.8728 5.4869 diff --git a/pandas/tests/io/pytables/common.py b/pandas/tests/io/pytables/common.py index d06f467760518..aad18890de3ad 100644 --- a/pandas/tests/io/pytables/common.py +++ b/pandas/tests/io/pytables/common.py @@ -74,8 +74,10 @@ def ensure_clean_path(path): def _maybe_remove(store, key): - """For tests using tables, try removing the table to be sure there is - no content from previous tests using the same table name.""" + """ + For tests using tables, try removing the table to be sure there is + no content from previous tests using the same table name. + """ try: store.remove(key) except (ValueError, KeyError): diff --git a/pandas/tests/io/pytables/conftest.py b/pandas/tests/io/pytables/conftest.py index 214f95c6fb441..38ffcb3b0e8ec 100644 --- a/pandas/tests/io/pytables/conftest.py +++ b/pandas/tests/io/pytables/conftest.py @@ -6,7 +6,7 @@ @pytest.fixture def setup_path(): """Fixture for setup path""" - return "tmp.__{}__.h5".format(tm.rands(10)) + return f"tmp.__{tm.rands(10)}__.h5" @pytest.fixture(scope="module", autouse=True) diff --git a/pandas/tests/io/pytables/test_store.py b/pandas/tests/io/pytables/test_store.py index f56d042093886..fd585a73f6ce6 100644 --- a/pandas/tests/io/pytables/test_store.py +++ b/pandas/tests/io/pytables/test_store.py @@ -653,7 +653,7 @@ def test_getattr(self, setup_path): # not stores for x in ["mode", "path", "handle", "complib"]: - getattr(store, "_{x}".format(x=x)) + getattr(store, f"_{x}") def test_put(self, setup_path): @@ -690,9 +690,7 @@ def test_put_string_index(self, setup_path): with ensure_clean_store(setup_path) as store: - index = Index( - ["I am a very long string index: {i}".format(i=i) for i in range(20)] - ) + index = Index([f"I am a very long string index: {i}" for i in range(20)]) s = Series(np.arange(20), index=index) df = DataFrame({"A": s, "B": s}) @@ -705,7 +703,7 @@ def test_put_string_index(self, setup_path): # mixed length index = Index( ["abcdefghijklmnopqrstuvwxyz1234567890"] - + ["I am a very long string index: {i}".format(i=i) for i in range(20)] + + [f"I am a very long string index: {i}" for i in range(20)] ) s = Series(np.arange(21), index=index) df = DataFrame({"A": s, "B": s}) @@ -2044,7 +2042,7 @@ def test_unimplemented_dtypes_table_columns(self, setup_path): df = tm.makeDataFrame() df[n] = f with pytest.raises(TypeError): - store.append("df1_{n}".format(n=n), df) + store.append(f"df1_{n}", df) # frame df = tm.makeDataFrame() @@ -2689,16 +2687,12 @@ def test_select_dtypes(self, setup_path): expected = df[df.boolv == True].reindex(columns=["A", "boolv"]) # noqa for v in [True, "true", 1]: - result = store.select( - "df", "boolv == {v!s}".format(v=v), columns=["A", "boolv"] - ) + result = store.select("df", f"boolv == {v}", columns=["A", "boolv"]) tm.assert_frame_equal(expected, result) expected = df[df.boolv == False].reindex(columns=["A", "boolv"]) # noqa for v in [False, "false", 0]: - result = store.select( - "df", "boolv == {v!s}".format(v=v), columns=["A", "boolv"] - ) + result = store.select("df", f"boolv == {v}", columns=["A", "boolv"]) tm.assert_frame_equal(expected, result) # integer index @@ -2784,7 +2778,7 @@ def test_select_with_many_inputs(self, setup_path): users=["a"] * 50 + ["b"] * 50 + ["c"] * 100 - + ["a{i:03d}".format(i=i) for i in range(100)], + + [f"a{i:03d}" for i in range(100)], ) ) _maybe_remove(store, "df") @@ -2805,7 +2799,7 @@ def test_select_with_many_inputs(self, setup_path): tm.assert_frame_equal(expected, result) # big selector along the columns - selector = ["a", "b", "c"] + ["a{i:03d}".format(i=i) for i in range(60)] + selector = ["a", "b", "c"] + [f"a{i:03d}" for i in range(60)] result = store.select( "df", "ts>=Timestamp('2012-02-01') and users=selector" ) @@ -2914,21 +2908,19 @@ def test_select_iterator_complete_8014(self, setup_path): # select w/o iterator and where clause, single term, begin # of range, works - where = "index >= '{beg_dt}'".format(beg_dt=beg_dt) + where = f"index >= '{beg_dt}'" result = store.select("df", where=where) tm.assert_frame_equal(expected, result) # select w/o iterator and where clause, single term, end # of range, works - where = "index <= '{end_dt}'".format(end_dt=end_dt) + where = f"index <= '{end_dt}'" result = store.select("df", where=where) tm.assert_frame_equal(expected, result) # select w/o iterator and where clause, inclusive range, # works - where = "index >= '{beg_dt}' & index <= '{end_dt}'".format( - beg_dt=beg_dt, end_dt=end_dt - ) + where = f"index >= '{beg_dt}' & index <= '{end_dt}'" result = store.select("df", where=where) tm.assert_frame_equal(expected, result) @@ -2948,21 +2940,19 @@ def test_select_iterator_complete_8014(self, setup_path): tm.assert_frame_equal(expected, result) # select w/iterator and where clause, single term, begin of range - where = "index >= '{beg_dt}'".format(beg_dt=beg_dt) + where = f"index >= '{beg_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) tm.assert_frame_equal(expected, result) # select w/iterator and where clause, single term, end of range - where = "index <= '{end_dt}'".format(end_dt=end_dt) + where = f"index <= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) tm.assert_frame_equal(expected, result) # select w/iterator and where clause, inclusive range - where = "index >= '{beg_dt}' & index <= '{end_dt}'".format( - beg_dt=beg_dt, end_dt=end_dt - ) + where = f"index >= '{beg_dt}' & index <= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) tm.assert_frame_equal(expected, result) @@ -2984,23 +2974,21 @@ def test_select_iterator_non_complete_8014(self, setup_path): end_dt = expected.index[-2] # select w/iterator and where clause, single term, begin of range - where = "index >= '{beg_dt}'".format(beg_dt=beg_dt) + where = f"index >= '{beg_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) rexpected = expected[expected.index >= beg_dt] tm.assert_frame_equal(rexpected, result) # select w/iterator and where clause, single term, end of range - where = "index <= '{end_dt}'".format(end_dt=end_dt) + where = f"index <= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) rexpected = expected[expected.index <= end_dt] tm.assert_frame_equal(rexpected, result) # select w/iterator and where clause, inclusive range - where = "index >= '{beg_dt}' & index <= '{end_dt}'".format( - beg_dt=beg_dt, end_dt=end_dt - ) + where = f"index >= '{beg_dt}' & index <= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) rexpected = expected[ @@ -3018,7 +3006,7 @@ def test_select_iterator_non_complete_8014(self, setup_path): end_dt = expected.index[-1] # select w/iterator and where clause, single term, begin of range - where = "index > '{end_dt}'".format(end_dt=end_dt) + where = f"index > '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) assert 0 == len(results) @@ -3040,14 +3028,14 @@ def test_select_iterator_many_empty_frames(self, setup_path): end_dt = expected.index[chunksize - 1] # select w/iterator and where clause, single term, begin of range - where = "index >= '{beg_dt}'".format(beg_dt=beg_dt) + where = f"index >= '{beg_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) result = concat(results) rexpected = expected[expected.index >= beg_dt] tm.assert_frame_equal(rexpected, result) # select w/iterator and where clause, single term, end of range - where = "index <= '{end_dt}'".format(end_dt=end_dt) + where = f"index <= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) assert len(results) == 1 @@ -3056,9 +3044,7 @@ def test_select_iterator_many_empty_frames(self, setup_path): tm.assert_frame_equal(rexpected, result) # select w/iterator and where clause, inclusive range - where = "index >= '{beg_dt}' & index <= '{end_dt}'".format( - beg_dt=beg_dt, end_dt=end_dt - ) + where = f"index >= '{beg_dt}' & index <= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) # should be 1, is 10 @@ -3076,9 +3062,7 @@ def test_select_iterator_many_empty_frames(self, setup_path): # return [] e.g. `for e in []: print True` never prints # True. - where = "index <= '{beg_dt}' & index >= '{end_dt}'".format( - beg_dt=beg_dt, end_dt=end_dt - ) + where = f"index <= '{beg_dt}' & index >= '{end_dt}'" results = list(store.select("df", where=where, chunksize=chunksize)) # should be [] @@ -3807,8 +3791,8 @@ def test_start_stop_fixed(self, setup_path): def test_select_filter_corner(self, setup_path): df = DataFrame(np.random.randn(50, 100)) - df.index = ["{c:3d}".format(c=c) for c in df.index] - df.columns = ["{c:3d}".format(c=c) for c in df.columns] + df.index = [f"{c:3d}" for c in df.index] + df.columns = [f"{c:3d}" for c in df.columns] with ensure_clean_store(setup_path) as store: store.put("frame", df, format="table") @@ -4074,6 +4058,21 @@ def test_legacy_table_fixed_format_read_py2(self, datapath, setup_path): ) tm.assert_frame_equal(expected, result) + def test_legacy_table_fixed_format_read_datetime_py2(self, datapath, setup_path): + # GH 31750 + # legacy table with fixed format and datetime64 column written in Python 2 + with ensure_clean_store( + datapath("io", "data", "legacy_hdf", "legacy_table_fixed_datetime_py2.h5"), + mode="r", + ) as store: + result = store.select("df") + expected = pd.DataFrame( + [[pd.Timestamp("2020-02-06T18:00")]], + columns=["A"], + index=pd.Index(["date"]), + ) + tm.assert_frame_equal(expected, result) + def test_legacy_table_read_py2(self, datapath, setup_path): # issue: 24925 # legacy table written in Python 2 @@ -4244,7 +4243,7 @@ def test_append_with_diff_col_name_types_raises_value_error(self, setup_path): df5 = DataFrame({("1", 2, object): np.random.randn(10)}) with ensure_clean_store(setup_path) as store: - name = "df_{}".format(tm.rands(10)) + name = f"df_{tm.rands(10)}" store.append(name, df) for d in (df2, df3, df4, df5): @@ -4528,9 +4527,7 @@ def test_to_hdf_with_object_column_names(self, setup_path): with ensure_clean_path(setup_path) as path: with catch_warnings(record=True): df.to_hdf(path, "df", format="table", data_columns=True) - result = pd.read_hdf( - path, "df", where="index = [{0}]".format(df.index[0]) - ) + result = pd.read_hdf(path, "df", where=f"index = [{df.index[0]}]") assert len(result) def test_read_hdf_open_store(self, setup_path): @@ -4663,16 +4660,16 @@ def test_query_long_float_literal(self, setup_path): store.append("test", df, format="table", data_columns=True) cutoff = 1000000000.0006 - result = store.select("test", "A < {cutoff:.4f}".format(cutoff=cutoff)) + result = store.select("test", f"A < {cutoff:.4f}") assert result.empty cutoff = 1000000000.0010 - result = store.select("test", "A > {cutoff:.4f}".format(cutoff=cutoff)) + result = store.select("test", f"A > {cutoff:.4f}") expected = df.loc[[1, 2], :] tm.assert_frame_equal(expected, result) exact = 1000000000.0011 - result = store.select("test", "A == {exact:.4f}".format(exact=exact)) + result = store.select("test", f"A == {exact:.4f}") expected = df.loc[[1], :] tm.assert_frame_equal(expected, result) @@ -4699,21 +4696,21 @@ def test_query_compare_column_type(self, setup_path): for op in ["<", ">", "=="]: # non strings to string column always fail for v in [2.1, True, pd.Timestamp("2014-01-01"), pd.Timedelta(1, "s")]: - query = "date {op} v".format(op=op) + query = f"date {op} v" with pytest.raises(TypeError): store.select("test", where=query) # strings to other columns must be convertible to type v = "a" for col in ["int", "float", "real_date"]: - query = "{col} {op} v".format(op=op, col=col) + query = f"{col} {op} v" with pytest.raises(ValueError): store.select("test", where=query) for v, col in zip( ["1", "1.1", "2014-01-01"], ["int", "float", "real_date"] ): - query = "{col} {op} v".format(op=op, col=col) + query = f"{col} {op} v" result = store.select("test", where=query) if op == "==": diff --git a/pandas/tests/io/pytables/test_timezones.py b/pandas/tests/io/pytables/test_timezones.py index 2bf22d982e5fe..74d5a77f86827 100644 --- a/pandas/tests/io/pytables/test_timezones.py +++ b/pandas/tests/io/pytables/test_timezones.py @@ -24,9 +24,7 @@ def _compare_with_tz(a, b): a_e = a.loc[i, c] b_e = b.loc[i, c] if not (a_e == b_e and a_e.tz == b_e.tz): - raise AssertionError( - "invalid tz comparison [{a_e}] [{b_e}]".format(a_e=a_e, b_e=b_e) - ) + raise AssertionError(f"invalid tz comparison [{a_e}] [{b_e}]") def test_append_with_timezones_dateutil(setup_path): diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index 652cacaf14ffb..3458cfb6ad254 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -114,7 +114,6 @@ def mock_clipboard(monkeypatch, request): This returns the local dictionary, for direct manipulation by tests. """ - # our local clipboard for tests _mock_data = {} diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 404f5a477187b..730043e6ec7d7 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -141,7 +141,24 @@ def test_read_non_existant(self, reader, module, error_class, fn_ext): pytest.importorskip(module) path = os.path.join(HERE, "data", "does_not_exist." + fn_ext) - with tm.external_error_raised(error_class): + msg1 = r"File (b')?.+does_not_exist\.{}'? does not exist".format(fn_ext) + msg2 = fr"\[Errno 2\] No such file or directory: '.+does_not_exist\.{fn_ext}'" + msg3 = "Expected object or value" + msg4 = "path_or_buf needs to be a string file path or file-like" + msg5 = ( + fr"\[Errno 2\] File .+does_not_exist\.{fn_ext} does not exist: " + fr"'.+does_not_exist\.{fn_ext}'" + ) + msg6 = fr"\[Errno 2\] 没有那个文件或目录: '.+does_not_exist\.{fn_ext}'" + msg7 = ( + fr"\[Errno 2\] File o directory non esistente: '.+does_not_exist\.{fn_ext}'" + ) + msg8 = fr"Failed to open local file.+does_not_exist\.{fn_ext}" + + with pytest.raises( + error_class, + match=fr"({msg1}|{msg2}|{msg3}|{msg4}|{msg5}|{msg6}|{msg7}|{msg8})", + ): reader(path) @pytest.mark.parametrize( @@ -167,7 +184,24 @@ def test_read_expands_user_home_dir( path = os.path.join("~", "does_not_exist." + fn_ext) monkeypatch.setattr(icom, "_expand_user", lambda x: os.path.join("foo", x)) - with tm.external_error_raised(error_class): + msg1 = fr"File (b')?.+does_not_exist\.{fn_ext}'? does not exist" + msg2 = fr"\[Errno 2\] No such file or directory: '.+does_not_exist\.{fn_ext}'" + msg3 = "Unexpected character found when decoding 'false'" + msg4 = "path_or_buf needs to be a string file path or file-like" + msg5 = ( + fr"\[Errno 2\] File .+does_not_exist\.{fn_ext} does not exist: " + fr"'.+does_not_exist\.{fn_ext}'" + ) + msg6 = fr"\[Errno 2\] 没有那个文件或目录: '.+does_not_exist\.{fn_ext}'" + msg7 = ( + fr"\[Errno 2\] File o directory non esistente: '.+does_not_exist\.{fn_ext}'" + ) + msg8 = fr"Failed to open local file.+does_not_exist\.{fn_ext}" + + with pytest.raises( + error_class, + match=fr"({msg1}|{msg2}|{msg3}|{msg4}|{msg5}|{msg6}|{msg7}|{msg8})", + ): reader(path) @pytest.mark.parametrize( diff --git a/pandas/tests/io/test_compression.py b/pandas/tests/io/test_compression.py index fb81e57912dac..841241d5124e0 100644 --- a/pandas/tests/io/test_compression.py +++ b/pandas/tests/io/test_compression.py @@ -129,7 +129,8 @@ def test_with_missing_lzma(): def test_with_missing_lzma_runtime(): """Tests if RuntimeError is hit when calling lzma without - having the module available.""" + having the module available. + """ code = textwrap.dedent( """ import sys diff --git a/pandas/tests/io/test_html.py b/pandas/tests/io/test_html.py index b649e394c780b..cbaf16d048eda 100644 --- a/pandas/tests/io/test_html.py +++ b/pandas/tests/io/test_html.py @@ -40,8 +40,8 @@ def html_encoding_file(request, datapath): def assert_framelist_equal(list1, list2, *args, **kwargs): assert len(list1) == len(list2), ( "lists are not of equal size " - "len(list1) == {0}, " - "len(list2) == {1}".format(len(list1), len(list2)) + f"len(list1) == {len(list1)}, " + f"len(list2) == {len(list2)}" ) msg = "not all list elements are DataFrames" both_frames = all( diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 7ed8d8f22764c..cfcf617cedf9c 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -151,7 +151,6 @@ def check_round_trip( repeat: int, optional How many times to repeat the test """ - write_kwargs = write_kwargs or {"compression": None} read_kwargs = read_kwargs or {} @@ -533,25 +532,28 @@ def test_additional_extension_arrays(self, pa): df = pd.DataFrame( { "a": pd.Series([1, 2, 3], dtype="Int64"), - "b": pd.Series(["a", None, "c"], dtype="string"), + "b": pd.Series([1, 2, 3], dtype="UInt32"), + "c": pd.Series(["a", None, "c"], dtype="string"), } ) - if LooseVersion(pyarrow.__version__) >= LooseVersion("0.15.1.dev"): + if LooseVersion(pyarrow.__version__) >= LooseVersion("0.16.0"): expected = df else: # de-serialized as plain int / object - expected = df.assign(a=df.a.astype("int64"), b=df.b.astype("object")) + expected = df.assign( + a=df.a.astype("int64"), b=df.b.astype("int64"), c=df.c.astype("object") + ) check_round_trip(df, pa, expected=expected) df = pd.DataFrame({"a": pd.Series([1, 2, 3, None], dtype="Int64")}) - if LooseVersion(pyarrow.__version__) >= LooseVersion("0.15.1.dev"): + if LooseVersion(pyarrow.__version__) >= LooseVersion("0.16.0"): expected = df else: # if missing values in integer, currently de-serialized as float expected = df.assign(a=df.a.astype("float64")) check_round_trip(df, pa, expected=expected) - @td.skip_if_no("pyarrow", min_version="0.15.1.dev") + @td.skip_if_no("pyarrow", min_version="0.16.0") def test_additional_extension_types(self, pa): # test additional ExtensionArrays that are supported through the # __arrow_array__ protocol + by defining a custom ExtensionType diff --git a/pandas/tests/io/test_pickle.py b/pandas/tests/io/test_pickle.py index 78b630bb5ada1..584a545769c4c 100644 --- a/pandas/tests/io/test_pickle.py +++ b/pandas/tests/io/test_pickle.py @@ -382,14 +382,23 @@ def test_read(self, protocol, get_random_path): tm.assert_frame_equal(df, df2) -def test_unicode_decode_error(datapath): +@pytest.mark.parametrize( + ["pickle_file", "excols"], + [ + ("test_py27.pkl", pd.Index(["a", "b", "c"])), + ( + "test_mi_py27.pkl", + pd.MultiIndex.from_arrays([["a", "b", "c"], ["A", "B", "C"]]), + ), + ], +) +def test_unicode_decode_error(datapath, pickle_file, excols): # pickle file written with py27, should be readable without raising - # UnicodeDecodeError, see GH#28645 - path = datapath("io", "data", "pickle", "test_py27.pkl") + # UnicodeDecodeError, see GH#28645 and GH#31988 + path = datapath("io", "data", "pickle", pickle_file) df = pd.read_pickle(path) # just test the columns are correct since the values are random - excols = pd.Index(["a", "b", "c"]) tm.assert_index_equal(df.columns, excols) diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index 45b3e839a08d1..fc3876eee9d66 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -2148,6 +2148,10 @@ def test_to_sql_replace(self): def test_to_sql_append(self): self._to_sql_append() + def test_to_sql_method_multi(self): + # GH 29921 + self._to_sql(method="multi") + def test_create_and_drop_table(self): temp_frame = DataFrame( {"one": [1.0, 2.0, 3.0, 4.0], "two": [4.0, 3.0, 2.0, 1.0]} @@ -2571,19 +2575,19 @@ def setup_class(cls): pymysql.connect(host="localhost", user="root", passwd="", db="pandas_nosetest") try: pymysql.connect(read_default_group="pandas") - except pymysql.ProgrammingError: + except pymysql.ProgrammingError as err: raise RuntimeError( "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " "typically located at ~/.my.cnf or /etc/.my.cnf." - ) - except pymysql.Error: + ) from err + except pymysql.Error as err: raise RuntimeError( "Cannot connect to database. " "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " "typically located at ~/.my.cnf or /etc/.my.cnf." - ) + ) from err @pytest.fixture(autouse=True) def setup_method(self, request, datapath): @@ -2591,19 +2595,19 @@ def setup_method(self, request, datapath): pymysql.connect(host="localhost", user="root", passwd="", db="pandas_nosetest") try: pymysql.connect(read_default_group="pandas") - except pymysql.ProgrammingError: + except pymysql.ProgrammingError as err: raise RuntimeError( "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " "typically located at ~/.my.cnf or /etc/.my.cnf." - ) - except pymysql.Error: + ) from err + except pymysql.Error as err: raise RuntimeError( "Cannot connect to database. " "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " "typically located at ~/.my.cnf or /etc/.my.cnf." - ) + ) from err self.method = request.function diff --git a/pandas/tests/io/test_stata.py b/pandas/tests/io/test_stata.py index cb2112b481952..b65efac2bd527 100644 --- a/pandas/tests/io/test_stata.py +++ b/pandas/tests/io/test_stata.py @@ -1715,7 +1715,7 @@ def test_invalid_file_not_written(self, version): "'ascii' codec can't decode byte 0xef in position 14: " r"ordinal not in range\(128\)" ) - with pytest.raises(UnicodeEncodeError, match=r"{}|{}".format(msg1, msg2)): + with pytest.raises(UnicodeEncodeError, match=f"{msg1}|{msg2}"): with tm.assert_produces_warning(ResourceWarning): df.to_stata(path) diff --git a/pandas/tests/plotting/common.py b/pandas/tests/plotting/common.py index a604d90acc854..ea0ec8ad98ffe 100644 --- a/pandas/tests/plotting/common.py +++ b/pandas/tests/plotting/common.py @@ -92,7 +92,6 @@ def _check_legend_labels(self, axes, labels=None, visible=True): expected legend visibility. labels are checked only when visible is True """ - if visible and (labels is None): raise ValueError("labels must be specified when visible is True") axes = self._flatten_visible(axes) @@ -190,7 +189,6 @@ def _check_colors( Series used for color grouping key used for andrew_curves, parallel_coordinates, radviz test """ - from matplotlib.lines import Line2D from matplotlib.collections import Collection, PolyCollection, LineCollection diff --git a/pandas/tests/plotting/test_boxplot_method.py b/pandas/tests/plotting/test_boxplot_method.py index 8ee279f0e1f38..b84fcffe26991 100644 --- a/pandas/tests/plotting/test_boxplot_method.py +++ b/pandas/tests/plotting/test_boxplot_method.py @@ -203,6 +203,23 @@ def test_color_kwd_errors(self, dict_colors, msg): with pytest.raises(ValueError, match=msg): df.boxplot(color=dict_colors, return_type="dict") + @pytest.mark.parametrize( + "props, expected", + [ + ("boxprops", "boxes"), + ("whiskerprops", "whiskers"), + ("capprops", "caps"), + ("medianprops", "medians"), + ], + ) + def test_specified_props_kwd(self, props, expected): + # GH 30346 + df = DataFrame({k: np.random.random(100) for k in "ABC"}) + kwd = {props: dict(color="C1")} + result = df.boxplot(return_type="dict", **kwd) + + assert result[expected][0].get_color() == "C1" + @td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 1c429bafa9a19..ffbd135466709 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -2352,6 +2352,23 @@ def _check_colors(bp, box_c, whiskers_c, medians_c, caps_c="k", fliers_c=None): # Color contains invalid key results in ValueError df.plot.box(color=dict(boxes="red", xxxx="blue")) + @pytest.mark.parametrize( + "props, expected", + [ + ("boxprops", "boxes"), + ("whiskerprops", "whiskers"), + ("capprops", "caps"), + ("medianprops", "medians"), + ], + ) + def test_specified_props_kwd_plot_box(self, props, expected): + # GH 30346 + df = DataFrame({k: np.random.random(100) for k in "ABC"}) + kwd = {props: dict(color="C1")} + result = df.plot.box(return_type="dict", **kwd) + + assert result[expected][0].get_color() == "C1" + def test_default_color_cycle(self): import matplotlib.pyplot as plt import cycler diff --git a/pandas/tests/reductions/test_reductions.py b/pandas/tests/reductions/test_reductions.py index 0b312fe2f8990..211d0d52d8357 100644 --- a/pandas/tests/reductions/test_reductions.py +++ b/pandas/tests/reductions/test_reductions.py @@ -66,60 +66,64 @@ def test_ops(self, opname, obj): expected = expected.astype("M8[ns]").astype("int64") assert result.value == expected - def test_nanops(self): + @pytest.mark.parametrize("opname", ["max", "min"]) + def test_nanops(self, opname, index_or_series): # GH#7261 - for opname in ["max", "min"]: - for klass in [Index, Series]: - arg_op = "arg" + opname if klass is Index else "idx" + opname - - obj = klass([np.nan, 2.0]) - assert getattr(obj, opname)() == 2.0 - - obj = klass([np.nan]) - assert pd.isna(getattr(obj, opname)()) - assert pd.isna(getattr(obj, opname)(skipna=False)) - - obj = klass([], dtype=object) - assert pd.isna(getattr(obj, opname)()) - assert pd.isna(getattr(obj, opname)(skipna=False)) - - obj = klass([pd.NaT, datetime(2011, 11, 1)]) - # check DatetimeIndex monotonic path - assert getattr(obj, opname)() == datetime(2011, 11, 1) - assert getattr(obj, opname)(skipna=False) is pd.NaT - - assert getattr(obj, arg_op)() == 1 - result = getattr(obj, arg_op)(skipna=False) - if klass is Series: - assert np.isnan(result) - else: - assert result == -1 - - obj = klass([pd.NaT, datetime(2011, 11, 1), pd.NaT]) - # check DatetimeIndex non-monotonic path - assert getattr(obj, opname)(), datetime(2011, 11, 1) - assert getattr(obj, opname)(skipna=False) is pd.NaT - - assert getattr(obj, arg_op)() == 1 - result = getattr(obj, arg_op)(skipna=False) - if klass is Series: - assert np.isnan(result) - else: - assert result == -1 - - for dtype in ["M8[ns]", "datetime64[ns, UTC]"]: - # cases with empty Series/DatetimeIndex - obj = klass([], dtype=dtype) - - assert getattr(obj, opname)() is pd.NaT - assert getattr(obj, opname)(skipna=False) is pd.NaT - - with pytest.raises(ValueError, match="empty sequence"): - getattr(obj, arg_op)() - with pytest.raises(ValueError, match="empty sequence"): - getattr(obj, arg_op)(skipna=False) - - # argmin/max + klass = index_or_series + arg_op = "arg" + opname if klass is Index else "idx" + opname + + obj = klass([np.nan, 2.0]) + assert getattr(obj, opname)() == 2.0 + + obj = klass([np.nan]) + assert pd.isna(getattr(obj, opname)()) + assert pd.isna(getattr(obj, opname)(skipna=False)) + + obj = klass([], dtype=object) + assert pd.isna(getattr(obj, opname)()) + assert pd.isna(getattr(obj, opname)(skipna=False)) + + obj = klass([pd.NaT, datetime(2011, 11, 1)]) + # check DatetimeIndex monotonic path + assert getattr(obj, opname)() == datetime(2011, 11, 1) + assert getattr(obj, opname)(skipna=False) is pd.NaT + + assert getattr(obj, arg_op)() == 1 + result = getattr(obj, arg_op)(skipna=False) + if klass is Series: + assert np.isnan(result) + else: + assert result == -1 + + obj = klass([pd.NaT, datetime(2011, 11, 1), pd.NaT]) + # check DatetimeIndex non-monotonic path + assert getattr(obj, opname)(), datetime(2011, 11, 1) + assert getattr(obj, opname)(skipna=False) is pd.NaT + + assert getattr(obj, arg_op)() == 1 + result = getattr(obj, arg_op)(skipna=False) + if klass is Series: + assert np.isnan(result) + else: + assert result == -1 + + @pytest.mark.parametrize("opname", ["max", "min"]) + @pytest.mark.parametrize("dtype", ["M8[ns]", "datetime64[ns, UTC]"]) + def test_nanops_empty_object(self, opname, index_or_series, dtype): + klass = index_or_series + arg_op = "arg" + opname if klass is Index else "idx" + opname + + obj = klass([], dtype=dtype) + + assert getattr(obj, opname)() is pd.NaT + assert getattr(obj, opname)(skipna=False) is pd.NaT + + with pytest.raises(ValueError, match="empty sequence"): + getattr(obj, arg_op)() + with pytest.raises(ValueError, match="empty sequence"): + getattr(obj, arg_op)(skipna=False) + + def test_argminmax(self): obj = Index(np.arange(5, dtype="int64")) assert obj.argmin() == 0 assert obj.argmax() == 4 @@ -224,16 +228,17 @@ def test_minmax_timedelta64(self): assert idx.argmin() == 0 assert idx.argmax() == 2 - for op in ["min", "max"]: - # Return NaT - obj = TimedeltaIndex([]) - assert pd.isna(getattr(obj, op)()) + @pytest.mark.parametrize("op", ["min", "max"]) + def test_minmax_timedelta_empty_or_na(self, op): + # Return NaT + obj = TimedeltaIndex([]) + assert getattr(obj, op)() is pd.NaT - obj = TimedeltaIndex([pd.NaT]) - assert pd.isna(getattr(obj, op)()) + obj = TimedeltaIndex([pd.NaT]) + assert getattr(obj, op)() is pd.NaT - obj = TimedeltaIndex([pd.NaT, pd.NaT, pd.NaT]) - assert pd.isna(getattr(obj, op)()) + obj = TimedeltaIndex([pd.NaT, pd.NaT, pd.NaT]) + assert getattr(obj, op)() is pd.NaT def test_numpy_minmax_timedelta64(self): td = timedelta_range("16815 days", "16820 days", freq="D") diff --git a/pandas/tests/resample/conftest.py b/pandas/tests/resample/conftest.py index bb4f7ced3350f..d5b71a6e4cee1 100644 --- a/pandas/tests/resample/conftest.py +++ b/pandas/tests/resample/conftest.py @@ -98,60 +98,76 @@ def _index_name(): @pytest.fixture def index(_index_factory, _index_start, _index_end, _index_freq, _index_name): - """Fixture for parametrization of date_range, period_range and - timedelta_range indexes""" + """ + Fixture for parametrization of date_range, period_range and + timedelta_range indexes + """ return _index_factory(_index_start, _index_end, freq=_index_freq, name=_index_name) @pytest.fixture def _static_values(index): - """Fixture for parametrization of values used in parametrization of + """ + Fixture for parametrization of values used in parametrization of Series and DataFrames with date_range, period_range and - timedelta_range indexes""" + timedelta_range indexes + """ return np.arange(len(index)) @pytest.fixture def _series_name(): - """Fixture for parametrization of Series name for Series used with - date_range, period_range and timedelta_range indexes""" + """ + Fixture for parametrization of Series name for Series used with + date_range, period_range and timedelta_range indexes + """ return None @pytest.fixture def series(index, _series_name, _static_values): - """Fixture for parametrization of Series with date_range, period_range and - timedelta_range indexes""" + """ + Fixture for parametrization of Series with date_range, period_range and + timedelta_range indexes + """ return Series(_static_values, index=index, name=_series_name) @pytest.fixture def empty_series(series): - """Fixture for parametrization of empty Series with date_range, - period_range and timedelta_range indexes""" + """ + Fixture for parametrization of empty Series with date_range, + period_range and timedelta_range indexes + """ return series[:0] @pytest.fixture def frame(index, _series_name, _static_values): - """Fixture for parametrization of DataFrame with date_range, period_range - and timedelta_range indexes""" + """ + Fixture for parametrization of DataFrame with date_range, period_range + and timedelta_range indexes + """ # _series_name is intentionally unused return DataFrame({"value": _static_values}, index=index) @pytest.fixture def empty_frame(series): - """Fixture for parametrization of empty DataFrame with date_range, - period_range and timedelta_range indexes""" + """ + Fixture for parametrization of empty DataFrame with date_range, + period_range and timedelta_range indexes + """ index = series.index[:0] return DataFrame(index=index) @pytest.fixture(params=[Series, DataFrame]) def series_and_frame(request, series, frame): - """Fixture for parametrization of Series and DataFrame with date_range, - period_range and timedelta_range indexes""" + """ + Fixture for parametrization of Series and DataFrame with date_range, + period_range and timedelta_range indexes + """ if request.param == Series: return series if request.param == DataFrame: diff --git a/pandas/tests/resample/test_base.py b/pandas/tests/resample/test_base.py index f8a1810e66219..c84a5bf653b0a 100644 --- a/pandas/tests/resample/test_base.py +++ b/pandas/tests/resample/test_base.py @@ -11,6 +11,7 @@ from pandas.core.indexes.datetimes import date_range from pandas.core.indexes.period import PeriodIndex, period_range from pandas.core.indexes.timedeltas import TimedeltaIndex, timedelta_range +from pandas.core.resample import _asfreq_compat # a fixture value can be overridden by the test parameter value. Note that the # value of the fixture can be overridden this way even if the test doesn't use @@ -103,10 +104,8 @@ def test_resample_empty_series(freq, empty_series, resample_method): result = getattr(s.resample(freq), resample_method)() expected = s.copy() - if isinstance(s.index, PeriodIndex): - expected.index = s.index.asfreq(freq=freq) - else: - expected.index = s.index._shallow_copy(freq=freq) + expected.index = _asfreq_compat(s.index, freq) + tm.assert_index_equal(result.index, expected.index) assert result.index.freq == expected.index.freq tm.assert_series_equal(result, expected, check_dtype=False) @@ -119,10 +118,8 @@ def test_resample_count_empty_series(freq, empty_series, resample_method): # GH28427 result = getattr(empty_series.resample(freq), resample_method)() - if isinstance(empty_series.index, PeriodIndex): - index = empty_series.index.asfreq(freq=freq) - else: - index = empty_series.index._shallow_copy(freq=freq) + index = _asfreq_compat(empty_series.index, freq) + expected = pd.Series([], dtype="int64", index=index, name=empty_series.name) tm.assert_series_equal(result, expected) @@ -141,10 +138,8 @@ def test_resample_empty_dataframe(empty_frame, freq, resample_method): # GH14962 expected = Series([], dtype=object) - if isinstance(df.index, PeriodIndex): - expected.index = df.index.asfreq(freq=freq) - else: - expected.index = df.index._shallow_copy(freq=freq) + expected.index = _asfreq_compat(df.index, freq) + tm.assert_index_equal(result.index, expected.index) assert result.index.freq == expected.index.freq tm.assert_almost_equal(result, expected, check_dtype=False) @@ -162,10 +157,8 @@ def test_resample_count_empty_dataframe(freq, empty_frame): result = empty_frame.resample(freq).count() - if isinstance(empty_frame.index, PeriodIndex): - index = empty_frame.index.asfreq(freq=freq) - else: - index = empty_frame.index._shallow_copy(freq=freq) + index = _asfreq_compat(empty_frame.index, freq) + expected = pd.DataFrame({"a": []}, dtype="int64", index=index) tm.assert_frame_equal(result, expected) @@ -181,10 +174,8 @@ def test_resample_size_empty_dataframe(freq, empty_frame): result = empty_frame.resample(freq).size() - if isinstance(empty_frame.index, PeriodIndex): - index = empty_frame.index.asfreq(freq=freq) - else: - index = empty_frame.index._shallow_copy(freq=freq) + index = _asfreq_compat(empty_frame.index, freq) + expected = pd.Series([], dtype="int64", index=index) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index ff303b808f6f5..70b65209db955 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -96,9 +96,7 @@ def test_selection(self, index, freq, kind, kwargs): def test_annual_upsample_cases( self, targ, conv, meth, month, simple_period_range_series ): - ts = simple_period_range_series( - "1/1/1990", "12/31/1991", freq="A-{month}".format(month=month) - ) + ts = simple_period_range_series("1/1/1990", "12/31/1991", freq=f"A-{month}") result = getattr(ts.resample(targ, convention=conv), meth)() expected = result.to_timestamp(targ, how=conv) @@ -130,9 +128,9 @@ def test_not_subperiod(self, simple_period_range_series, rule, expected_error_ms # These are incompatible period rules for resampling ts = simple_period_range_series("1/1/1990", "6/30/1995", freq="w-wed") msg = ( - "Frequency cannot be resampled to {}, as they " - "are not sub or super periods" - ).format(expected_error_msg) + "Frequency cannot be resampled to " + f"{expected_error_msg}, as they are not sub or super periods" + ) with pytest.raises(IncompatibleFrequency, match=msg): ts.resample(rule).mean() @@ -176,7 +174,7 @@ def test_annual_upsample(self, simple_period_range_series): def test_quarterly_upsample( self, month, target, convention, simple_period_range_series ): - freq = "Q-{month}".format(month=month) + freq = f"Q-{month}" ts = simple_period_range_series("1/1/1990", "12/31/1995", freq=freq) result = ts.resample(target, convention=convention).ffill() expected = result.to_timestamp(target, how=convention) @@ -351,7 +349,7 @@ def test_fill_method_and_how_upsample(self): @pytest.mark.parametrize("target", ["D", "B"]) @pytest.mark.parametrize("convention", ["start", "end"]) def test_weekly_upsample(self, day, target, convention, simple_period_range_series): - freq = "W-{day}".format(day=day) + freq = f"W-{day}" ts = simple_period_range_series("1/1/1990", "12/31/1995", freq=freq) result = ts.resample(target, convention=convention).ffill() expected = result.to_timestamp(target, how=convention) @@ -367,16 +365,14 @@ def test_resample_to_timestamps(self, simple_period_range_series): def test_resample_to_quarterly(self, simple_period_range_series): for month in MONTHS: - ts = simple_period_range_series( - "1990", "1992", freq="A-{month}".format(month=month) - ) - quar_ts = ts.resample("Q-{month}".format(month=month)).ffill() + ts = simple_period_range_series("1990", "1992", freq=f"A-{month}") + quar_ts = ts.resample(f"Q-{month}").ffill() stamps = ts.to_timestamp("D", how="start") qdates = period_range( ts.index[0].asfreq("D", "start"), ts.index[-1].asfreq("D", "end"), - freq="Q-{month}".format(month=month), + freq=f"Q-{month}", ) expected = stamps.reindex(qdates.to_timestamp("D", "s"), method="ffill") diff --git a/pandas/tests/resample/test_time_grouper.py b/pandas/tests/resample/test_time_grouper.py index 3aa7765954634..bf998a6e83909 100644 --- a/pandas/tests/resample/test_time_grouper.py +++ b/pandas/tests/resample/test_time_grouper.py @@ -119,7 +119,6 @@ def test_aaa_group_order(): def test_aggregate_normal(resample_method): """Check TimeGrouper's aggregation is identical as normal groupby.""" - if resample_method == "ohlc": pytest.xfail(reason="DataError: No numeric types to aggregate") diff --git a/pandas/tests/reshape/merge/test_join.py b/pandas/tests/reshape/merge/test_join.py index 7020d373caf82..725157b7c8523 100644 --- a/pandas/tests/reshape/merge/test_join.py +++ b/pandas/tests/reshape/merge/test_join.py @@ -262,8 +262,9 @@ def test_join_on_fails_with_wrong_object_type(self, wrong_type): # Edited test to remove the Series object from test parameters df = DataFrame({"a": [1, 1]}) - msg = "Can only merge Series or DataFrame objects, a {} was passed".format( - str(type(wrong_type)) + msg = ( + "Can only merge Series or DataFrame objects, " + f"a {type(wrong_type)} was passed" ) with pytest.raises(TypeError, match=msg): merge(wrong_type, df, left_on="a", right_on="a") @@ -809,13 +810,11 @@ def _check_join(left, right, result, join_col, how="left", lsuffix="_x", rsuffix try: lgroup = left_grouped.get_group(group_key) - except KeyError: + except KeyError as err: if how in ("left", "inner"): raise AssertionError( - "key {group_key!s} should not have been in the join".format( - group_key=group_key - ) - ) + f"key {group_key} should not have been in the join" + ) from err _assert_all_na(l_joined, left.columns, join_col) else: @@ -823,13 +822,11 @@ def _check_join(left, right, result, join_col, how="left", lsuffix="_x", rsuffix try: rgroup = right_grouped.get_group(group_key) - except KeyError: + except KeyError as err: if how in ("right", "inner"): raise AssertionError( - "key {group_key!s} should not have been in the join".format( - group_key=group_key - ) - ) + f"key {group_key} should not have been in the join" + ) from err _assert_all_na(r_joined, right.columns, join_col) else: diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py index fd189c7435b29..d80e2e7afceef 100644 --- a/pandas/tests/reshape/merge/test_merge.py +++ b/pandas/tests/reshape/merge/test_merge.py @@ -710,7 +710,7 @@ def test_other_timedelta_unit(self, unit): df1 = pd.DataFrame({"entity_id": [101, 102]}) s = pd.Series([None, None], index=[101, 102], name="days") - dtype = "m8[{}]".format(unit) + dtype = f"m8[{unit}]" df2 = s.astype(dtype).to_frame("days") assert df2["days"].dtype == "m8[ns]" @@ -1012,9 +1012,9 @@ def test_indicator(self): msg = ( "Cannot use `indicator=True` option when data contains a " - "column named {}|" + f"column named {i}|" "Cannot use name of an existing column for indicator column" - ).format(i) + ) with pytest.raises(ValueError, match=msg): merge(df1, df_badcolumn, on="col1", how="outer", indicator=True) with pytest.raises(ValueError, match=msg): @@ -1555,11 +1555,9 @@ def test_merge_incompat_dtypes_error(self, df1_vals, df2_vals): df2 = DataFrame({"A": df2_vals}) msg = ( - "You are trying to merge on {lk_dtype} and " - "{rk_dtype} columns. If you wish to proceed " - "you should use pd.concat".format( - lk_dtype=df1["A"].dtype, rk_dtype=df2["A"].dtype - ) + f"You are trying to merge on {df1['A'].dtype} and " + f"{df2['A'].dtype} columns. If you wish to proceed " + "you should use pd.concat" ) msg = re.escape(msg) with pytest.raises(ValueError, match=msg): @@ -1567,11 +1565,9 @@ def test_merge_incompat_dtypes_error(self, df1_vals, df2_vals): # Check that error still raised when swapping order of dataframes msg = ( - "You are trying to merge on {lk_dtype} and " - "{rk_dtype} columns. If you wish to proceed " - "you should use pd.concat".format( - lk_dtype=df2["A"].dtype, rk_dtype=df1["A"].dtype - ) + f"You are trying to merge on {df2['A'].dtype} and " + f"{df1['A'].dtype} columns. If you wish to proceed " + "you should use pd.concat" ) msg = re.escape(msg) with pytest.raises(ValueError, match=msg): @@ -2167,3 +2163,25 @@ def test_merge_datetime_upcast_dtype(): } ) tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("n_categories", [5, 128]) +def test_categorical_non_unique_monotonic(n_categories): + # GH 28189 + # With n_categories as 5, we test the int8 case is hit in libjoin, + # with n_categories as 128 we test the int16 case. + left_index = CategoricalIndex([0] + list(range(n_categories))) + df1 = DataFrame(range(n_categories + 1), columns=["value"], index=left_index) + df2 = DataFrame( + [[6]], + columns=["value"], + index=CategoricalIndex([0], categories=np.arange(n_categories)), + ) + + result = merge(df1, df2, how="left", left_index=True, right_index=True) + expected = DataFrame( + [[i, 6.0] if i < 2 else [i, np.nan] for i in range(n_categories + 1)], + columns=["value_x", "value_y"], + index=left_index, + ) + tm.assert_frame_equal(expected, result) diff --git a/pandas/tests/reshape/merge/test_merge_asof.py b/pandas/tests/reshape/merge/test_merge_asof.py index 8037095aff0b9..9b09f0033715d 100644 --- a/pandas/tests/reshape/merge/test_merge_asof.py +++ b/pandas/tests/reshape/merge/test_merge_asof.py @@ -35,7 +35,6 @@ def setup_method(self, datapath): def test_examples1(self): """ doc-string examples """ - left = pd.DataFrame({"a": [1, 5, 10], "left_val": ["a", "b", "c"]}) right = pd.DataFrame({"a": [1, 2, 3, 6, 7], "right_val": [1, 2, 3, 6, 7]}) @@ -48,7 +47,6 @@ def test_examples1(self): def test_examples2(self): """ doc-string examples """ - trades = pd.DataFrame( { "time": pd.to_datetime( @@ -1198,7 +1196,7 @@ def test_merge_groupby_multiple_column_with_categorical_column(self): @pytest.mark.parametrize("side", ["left", "right"]) def test_merge_on_nans(self, func, side): # GH 23189 - msg = "Merge keys contain null values on {} side".format(side) + msg = f"Merge keys contain null values on {side} side" nulls = func([1.0, 5.0, np.nan]) non_nulls = func([1.0, 5.0, 10.0]) df_null = pd.DataFrame({"a": nulls, "left_val": ["a", "b", "c"]}) diff --git a/pandas/tests/reshape/merge/test_merge_index_as_string.py b/pandas/tests/reshape/merge/test_merge_index_as_string.py index 691f2549c0ece..08614d04caf4b 100644 --- a/pandas/tests/reshape/merge/test_merge_index_as_string.py +++ b/pandas/tests/reshape/merge/test_merge_index_as_string.py @@ -30,7 +30,8 @@ def df2(): @pytest.fixture(params=[[], ["outer"], ["outer", "inner"]]) def left_df(request, df1): """ Construct left test DataFrame with specified levels - (any of 'outer', 'inner', and 'v1')""" + (any of 'outer', 'inner', and 'v1') + """ levels = request.param if levels: df1 = df1.set_index(levels) @@ -41,7 +42,8 @@ def left_df(request, df1): @pytest.fixture(params=[[], ["outer"], ["outer", "inner"]]) def right_df(request, df2): """ Construct right test DataFrame with specified levels - (any of 'outer', 'inner', and 'v2')""" + (any of 'outer', 'inner', and 'v2') + """ levels = request.param if levels: @@ -80,7 +82,6 @@ def compute_expected(df_left, df_right, on=None, left_on=None, right_on=None, ho DataFrame The expected merge result """ - # Handle on param if specified if on is not None: left_on, right_on = on, on diff --git a/pandas/tests/reshape/test_concat.py b/pandas/tests/reshape/test_concat.py index 5811f3bc196a1..afd8f4178f741 100644 --- a/pandas/tests/reshape/test_concat.py +++ b/pandas/tests/reshape/test_concat.py @@ -1849,8 +1849,8 @@ def __len__(self) -> int: def __getitem__(self, index): try: return {0: df1, 1: df2}[index] - except KeyError: - raise IndexError + except KeyError as err: + raise IndexError from err tm.assert_frame_equal(pd.concat(CustomIterator1(), ignore_index=True), expected) diff --git a/pandas/tests/reshape/test_melt.py b/pandas/tests/reshape/test_melt.py index 814325844cb4c..6a670e6c729e9 100644 --- a/pandas/tests/reshape/test_melt.py +++ b/pandas/tests/reshape/test_melt.py @@ -364,8 +364,8 @@ def test_pairs(self): df = DataFrame(data) spec = { - "visitdt": ["visitdt{i:d}".format(i=i) for i in range(1, 4)], - "wt": ["wt{i:d}".format(i=i) for i in range(1, 4)], + "visitdt": [f"visitdt{i:d}" for i in range(1, 4)], + "wt": [f"wt{i:d}" for i in range(1, 4)], } result = lreshape(df, spec) @@ -557,8 +557,8 @@ def test_pairs(self): result = lreshape(df, spec, dropna=False, label="foo") spec = { - "visitdt": ["visitdt{i:d}".format(i=i) for i in range(1, 3)], - "wt": ["wt{i:d}".format(i=i) for i in range(1, 4)], + "visitdt": [f"visitdt{i:d}" for i in range(1, 3)], + "wt": [f"wt{i:d}" for i in range(1, 4)], } msg = "All column lists must be same length" with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index fe75aef1ca3d7..e09a2a7907177 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -1161,9 +1161,9 @@ def test_margins_no_values_two_row_two_cols(self): def test_pivot_table_with_margins_set_margin_name(self, margin_name): # see gh-3335 msg = ( - r'Conflicting name "{}" in margins|' + f'Conflicting name "{margin_name}" in margins|' "margins_name argument must be a string" - ).format(margin_name) + ) with pytest.raises(ValueError, match=msg): # multi-index index pivot_table( diff --git a/pandas/tests/reshape/test_pivot_multilevel.py b/pandas/tests/reshape/test_pivot_multilevel.py new file mode 100644 index 0000000000000..8374e829e6a28 --- /dev/null +++ b/pandas/tests/reshape/test_pivot_multilevel.py @@ -0,0 +1,192 @@ +import numpy as np +import pytest + +import pandas as pd +from pandas import Index, MultiIndex +import pandas._testing as tm + + +@pytest.mark.parametrize( + "input_index, input_columns, input_values, " + "expected_values, expected_columns, expected_index", + [ + ( + ["lev4"], + "lev3", + "values", + [ + [0.0, np.nan], + [np.nan, 1.0], + [2.0, np.nan], + [np.nan, 3.0], + [4.0, np.nan], + [np.nan, 5.0], + [6.0, np.nan], + [np.nan, 7.0], + ], + Index([1, 2], name="lev3"), + Index([1, 2, 3, 4, 5, 6, 7, 8], name="lev4"), + ), + ( + ["lev4"], + "lev3", + None, + [ + [1.0, np.nan, 1.0, np.nan, 0.0, np.nan], + [np.nan, 1.0, np.nan, 1.0, np.nan, 1.0], + [1.0, np.nan, 2.0, np.nan, 2.0, np.nan], + [np.nan, 1.0, np.nan, 2.0, np.nan, 3.0], + [2.0, np.nan, 1.0, np.nan, 4.0, np.nan], + [np.nan, 2.0, np.nan, 1.0, np.nan, 5.0], + [2.0, np.nan, 2.0, np.nan, 6.0, np.nan], + [np.nan, 2.0, np.nan, 2.0, np.nan, 7.0], + ], + MultiIndex.from_tuples( + [ + ("lev1", 1), + ("lev1", 2), + ("lev2", 1), + ("lev2", 2), + ("values", 1), + ("values", 2), + ], + names=[None, "lev3"], + ), + Index([1, 2, 3, 4, 5, 6, 7, 8], name="lev4"), + ), + ( + ["lev1", "lev2"], + "lev3", + "values", + [[0, 1], [2, 3], [4, 5], [6, 7]], + Index([1, 2], name="lev3"), + MultiIndex.from_tuples( + [(1, 1), (1, 2), (2, 1), (2, 2)], names=["lev1", "lev2"] + ), + ), + ( + ["lev1", "lev2"], + "lev3", + None, + [[1, 2, 0, 1], [3, 4, 2, 3], [5, 6, 4, 5], [7, 8, 6, 7]], + MultiIndex.from_tuples( + [("lev4", 1), ("lev4", 2), ("values", 1), ("values", 2)], + names=[None, "lev3"], + ), + MultiIndex.from_tuples( + [(1, 1), (1, 2), (2, 1), (2, 2)], names=["lev1", "lev2"] + ), + ), + ], +) +def test_pivot_list_like_index( + input_index, + input_columns, + input_values, + expected_values, + expected_columns, + expected_index, +): + # GH 21425, test when index is given a list + df = pd.DataFrame( + { + "lev1": [1, 1, 1, 1, 2, 2, 2, 2], + "lev2": [1, 1, 2, 2, 1, 1, 2, 2], + "lev3": [1, 2, 1, 2, 1, 2, 1, 2], + "lev4": [1, 2, 3, 4, 5, 6, 7, 8], + "values": [0, 1, 2, 3, 4, 5, 6, 7], + } + ) + + result = df.pivot(index=input_index, columns=input_columns, values=input_values) + expected = pd.DataFrame( + expected_values, columns=expected_columns, index=expected_index + ) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + "input_index, input_columns, input_values, " + "expected_values, expected_columns, expected_index", + [ + ( + "lev4", + ["lev3"], + "values", + [ + [0.0, np.nan], + [np.nan, 1.0], + [2.0, np.nan], + [np.nan, 3.0], + [4.0, np.nan], + [np.nan, 5.0], + [6.0, np.nan], + [np.nan, 7.0], + ], + Index([1, 2], name="lev3"), + Index([1, 2, 3, 4, 5, 6, 7, 8], name="lev4"), + ), + ( + ["lev1", "lev2"], + ["lev3"], + "values", + [[0, 1], [2, 3], [4, 5], [6, 7]], + Index([1, 2], name="lev3"), + MultiIndex.from_tuples( + [(1, 1), (1, 2), (2, 1), (2, 2)], names=["lev1", "lev2"] + ), + ), + ( + ["lev1"], + ["lev2", "lev3"], + "values", + [[0, 1, 2, 3], [4, 5, 6, 7]], + MultiIndex.from_tuples( + [(1, 1), (1, 2), (2, 1), (2, 2)], names=["lev2", "lev3"] + ), + Index([1, 2], name="lev1"), + ), + ( + ["lev1", "lev2"], + ["lev3", "lev4"], + "values", + [ + [0.0, 1.0, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan], + [np.nan, np.nan, 2.0, 3.0, np.nan, np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan, np.nan, 4.0, 5.0, np.nan, np.nan], + [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 6.0, 7.0], + ], + MultiIndex.from_tuples( + [(1, 1), (2, 2), (1, 3), (2, 4), (1, 5), (2, 6), (1, 7), (2, 8)], + names=["lev3", "lev4"], + ), + MultiIndex.from_tuples( + [(1, 1), (1, 2), (2, 1), (2, 2)], names=["lev1", "lev2"] + ), + ), + ], +) +def test_pivot_list_like_columns( + input_index, + input_columns, + input_values, + expected_values, + expected_columns, + expected_index, +): + # GH 21425, test when columns is given a list + df = pd.DataFrame( + { + "lev1": [1, 1, 1, 1, 2, 2, 2, 2], + "lev2": [1, 1, 2, 2, 1, 1, 2, 2], + "lev3": [1, 2, 1, 2, 1, 2, 1, 2], + "lev4": [1, 2, 3, 4, 5, 6, 7, 8], + "values": [0, 1, 2, 3, 4, 5, 6, 7], + } + ) + + result = df.pivot(index=input_index, columns=input_columns, values=input_values) + expected = pd.DataFrame( + expected_values, columns=expected_columns, index=expected_index + ) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 995d47c1473be..3846274dacd75 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -650,7 +650,7 @@ def test_strftime(self): class TestPeriodProperties: - "Test properties such as year, month, weekday, etc...." + """Test properties such as year, month, weekday, etc....""" @pytest.mark.parametrize("freq", ["A", "M", "D", "H"]) def test_is_leap_year(self, freq): @@ -1565,3 +1565,21 @@ def test_small_year_parsing(): per1 = Period("0001-01-07", "D") assert per1.year == 1 assert per1.day == 7 + + +def test_negone_ordinals(): + freqs = ["A", "M", "Q", "D", "H", "T", "S"] + + period = Period(ordinal=-1, freq="D") + for freq in freqs: + repr(period.asfreq(freq)) + + for freq in freqs: + period = Period(ordinal=-1, freq=freq) + repr(period) + assert period.year == 1969 + + period = Period(ordinal=-1, freq="B") + repr(period) + period = Period(ordinal=-1, freq="W") + repr(period) diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index 5fc991df49424..230a14aeec60a 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -8,7 +8,7 @@ import pytest import pandas as pd -from pandas import NaT, Timedelta, Timestamp, offsets +from pandas import NaT, Timedelta, Timestamp, _is_numpy_dev, offsets import pandas._testing as tm from pandas.core import ops @@ -377,7 +377,21 @@ def test_td_div_numeric_scalar(self): assert isinstance(result, Timedelta) assert result == Timedelta(days=2) - @pytest.mark.parametrize("nan", [np.nan, np.float64("NaN"), float("nan")]) + @pytest.mark.parametrize( + "nan", + [ + np.nan, + pytest.param( + np.float64("NaN"), + marks=pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, + ), + ), + float("nan"), + ], + ) def test_td_div_nan(self, nan): # np.float64('NaN') has a 'dtype' attr, avoid treating as array td = Timedelta(10, unit="d") diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 25c9fc19981be..d32d1994cac74 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -239,7 +239,7 @@ def test_iso_constructor(fmt, exp): ], ) def test_iso_constructor_raises(fmt): - msg = "Invalid ISO 8601 Duration format - {}".format(fmt) + msg = f"Invalid ISO 8601 Duration format - {fmt}" with pytest.raises(ValueError, match=msg): Timedelta(fmt) diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index 737a85faa4c9b..4c75d1ebcd377 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -314,7 +314,7 @@ def test_constructor_nanosecond(self, result): def test_constructor_invalid_Z0_isostring(self, z): # GH 8910 with pytest.raises(ValueError): - Timestamp("2014-11-02 01:00{}".format(z)) + Timestamp(f"2014-11-02 01:00{z}") @pytest.mark.parametrize( "arg", @@ -455,9 +455,7 @@ def test_disallow_setting_tz(self, tz): @pytest.mark.parametrize("offset", ["+0300", "+0200"]) def test_construct_timestamp_near_dst(self, offset): # GH 20854 - expected = Timestamp( - "2016-10-30 03:00:00{}".format(offset), tz="Europe/Helsinki" - ) + expected = Timestamp(f"2016-10-30 03:00:00{offset}", tz="Europe/Helsinki") result = Timestamp(expected).tz_convert("Europe/Helsinki") assert result == expected @@ -550,3 +548,16 @@ def test_timestamp_constructor_identity(): expected = Timestamp("2017-01-01T12") result = Timestamp(expected) assert result is expected + + +@pytest.mark.parametrize("kwargs", [{}, {"year": 2020}, {"year": 2020, "month": 1}]) +def test_constructor_missing_keyword(kwargs): + # GH 31200 + + # The exact error message of datetime() depends on its version + msg1 = r"function missing required argument '(year|month|day)' \(pos [123]\)" + msg2 = r"Required argument '(year|month|day)' \(pos [123]\) not found" + msg = "|".join([msg1, msg2]) + + with pytest.raises(TypeError, match=msg): + Timestamp(**kwargs) diff --git a/pandas/tests/scalar/timestamp/test_rendering.py b/pandas/tests/scalar/timestamp/test_rendering.py index 6b64b230a0bb9..a27d233d5ab88 100644 --- a/pandas/tests/scalar/timestamp/test_rendering.py +++ b/pandas/tests/scalar/timestamp/test_rendering.py @@ -17,7 +17,7 @@ class TestTimestampRendering: ) def test_repr(self, date, freq, tz): # avoid to match with timezone name - freq_repr = "'{0}'".format(freq) + freq_repr = f"'{freq}'" if tz.startswith("dateutil"): tz_repr = tz.replace("dateutil", "") else: @@ -85,3 +85,13 @@ def test_pprint(self): {'w': {'a': Timestamp('2011-01-01 00:00:00')}}], 'foo': 1}""" assert result == expected + + def test_to_timestamp_repr_is_code(self): + zs = [ + Timestamp("99-04-17 00:00:00", tz="UTC"), + Timestamp("2001-04-17 00:00:00", tz="UTC"), + Timestamp("2001-04-17 00:00:00", tz="America/Los_Angeles"), + Timestamp("2001-04-17 00:00:00", tz=None), + ] + for z in zs: + assert eval(repr(z)) == z diff --git a/pandas/tests/scalar/timestamp/test_timezones.py b/pandas/tests/scalar/timestamp/test_timezones.py index 6537f6ccd8432..cfa7da810ada1 100644 --- a/pandas/tests/scalar/timestamp/test_timezones.py +++ b/pandas/tests/scalar/timestamp/test_timezones.py @@ -140,7 +140,7 @@ def test_tz_localize_ambiguous_compat(self): # see gh-14621 assert result_pytz.to_pydatetime().tzname() == "GMT" assert result_dateutil.to_pydatetime().tzname() == "BST" - assert str(result_pytz) != str(result_dateutil) + assert str(result_pytz) == str(result_dateutil) # 1 hour difference result_pytz = naive.tz_localize(pytz_zone, ambiguous=1) diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index 65066fd0099ba..78e795e71cd07 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -225,25 +225,24 @@ def test_round_dst_border_nonexistent(self, method, ts_str, freq): ], ) def test_round_int64(self, timestamp, freq): - """check that all rounding modes are accurate to int64 precision - see GH#22591 - """ + # 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 result.value % unit == 0, f"floor not a {freq} multiple" 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 result.value % unit == 0, f"ceil not a {freq} multiple" 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 result.value % unit == 0, f"round not a {freq} multiple" 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 diff --git a/pandas/tests/series/indexing/test_alter_index.py b/pandas/tests/series/indexing/test_alter_index.py index dc8b91de3d09b..05bd967903e9d 100644 --- a/pandas/tests/series/indexing/test_alter_index.py +++ b/pandas/tests/series/indexing/test_alter_index.py @@ -153,6 +153,17 @@ def test_align_multiindex(): tm.assert_series_equal(expr, res2l) +@pytest.mark.parametrize("method", ["backfill", "bfill", "pad", "ffill", None]) +def test_align_method(method): + # GH31788 + ser = pd.Series(range(3), index=range(3)) + df = pd.DataFrame(0.0, index=range(3), columns=range(3)) + + result_ser, result_df = ser.align(df, method=method) + tm.assert_series_equal(result_ser, ser) + tm.assert_frame_equal(result_df, df) + + def test_reindex(datetime_series, string_series): identity = string_series.reindex(string_series.index) diff --git a/pandas/tests/series/indexing/test_boolean.py b/pandas/tests/series/indexing/test_boolean.py index 28f3c0f7429f8..8878a4a6526af 100644 --- a/pandas/tests/series/indexing/test_boolean.py +++ b/pandas/tests/series/indexing/test_boolean.py @@ -72,7 +72,7 @@ def test_getitem_boolean_object(string_series): # nans raise exception omask[5:10] = np.nan - msg = "cannot mask with array containing NA / NaN values" + msg = "Cannot mask with non-boolean array containing NA / NaN values" with pytest.raises(ValueError, match=msg): s[omask] with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py index acaa9de88a836..fc9d4ec5290a5 100644 --- a/pandas/tests/series/indexing/test_datetime.py +++ b/pandas/tests/series/indexing/test_datetime.py @@ -73,17 +73,13 @@ def test_series_set_value(): dates = [datetime(2001, 1, 1), datetime(2001, 1, 2)] index = DatetimeIndex(dates) - s = Series(dtype=object)._set_value(dates[0], 1.0) - s2 = s._set_value(dates[1], np.nan) + s = Series(dtype=object) + s._set_value(dates[0], 1.0) + s._set_value(dates[1], np.nan) expected = Series([1.0, np.nan], index=index) - tm.assert_series_equal(s2, expected) - - # FIXME: dont leave commented-out - # s = Series(index[:1], index[:1]) - # s2 = s._set_value(dates[1], index[1]) - # assert s2.values.dtype == 'M8[ns]' + tm.assert_series_equal(s, expected) @pytest.mark.slow @@ -364,7 +360,9 @@ def test_getitem_median_slice_bug(): s = Series(np.random.randn(13), index=index) indexer = [slice(6, 7, None)] - result = s[indexer] + with tm.assert_produces_warning(FutureWarning): + # GH#31299 + result = s[indexer] expected = s[indexer[0]] tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/indexing/test_indexing.py b/pandas/tests/series/indexing/test_indexing.py index fa5c75d5e4ad9..18fcbea683dd3 100644 --- a/pandas/tests/series/indexing/test_indexing.py +++ b/pandas/tests/series/indexing/test_indexing.py @@ -241,6 +241,16 @@ def test_series_box_timestamp(): assert isinstance(ser.iat[5], pd.Timestamp) +def test_series_box_timedelta(): + rng = pd.timedelta_range("1 day 1 s", periods=5, freq="h") + ser = pd.Series(rng) + assert isinstance(ser[0], Timedelta) + assert isinstance(ser.at[1], Timedelta) + assert isinstance(ser.iat[2], Timedelta) + assert isinstance(ser.loc[3], Timedelta) + assert isinstance(ser.iloc[4], Timedelta) + + def test_getitem_ambiguous_keyerror(): s = Series(range(10), index=list(range(0, 20, 2))) with pytest.raises(KeyError, match=r"^1$"): @@ -365,15 +375,15 @@ def test_setitem_dtypes(): def test_set_value(datetime_series, string_series): idx = datetime_series.index[10] res = datetime_series._set_value(idx, 0) - assert res is datetime_series + assert res is None assert datetime_series[idx] == 0 # equiv s = string_series.copy() res = s._set_value("foobar", 0) - assert res is s - assert res.index[-1] == "foobar" - assert res["foobar"] == 0 + assert res is None + assert s.index[-1] == "foobar" + assert s["foobar"] == 0 s = string_series.copy() s.loc["foobar"] = 0 @@ -414,7 +424,9 @@ def test_basic_getitem_setitem_corner(datetime_series): datetime_series[:, 2] = 2 # weird lists. [slice(0, 5)] will work but not two slices - result = datetime_series[[slice(None, 5)]] + with tm.assert_produces_warning(FutureWarning): + # GH#31299 + result = datetime_series[[slice(None, 5)]] expected = datetime_series[:5] tm.assert_series_equal(result, expected) @@ -564,6 +576,18 @@ def test_categorical_assigning_ops(): tm.assert_series_equal(s, exp) +def test_getitem_categorical_str(): + # GH#31765 + ser = pd.Series(range(5), index=pd.Categorical(["a", "b", "c", "a", "b"])) + result = ser["a"] + expected = ser.iloc[[0, 3]] + tm.assert_series_equal(result, expected) + + # Check the intermediate steps work as expected + result = ser.index.get_value(ser, "a") + tm.assert_series_equal(result, expected) + + def test_slice(string_series, object_series): numSlice = string_series[10:20] numSliceEnd = string_series[-10:] diff --git a/pandas/tests/series/methods/test_append.py b/pandas/tests/series/methods/test_append.py index 4d64b5b397981..4742d6ae3544f 100644 --- a/pandas/tests/series/methods/test_append.py +++ b/pandas/tests/series/methods/test_append.py @@ -2,7 +2,7 @@ import pytest import pandas as pd -from pandas import DataFrame, DatetimeIndex, Series, date_range +from pandas import DataFrame, DatetimeIndex, Index, Series, Timestamp, date_range import pandas._testing as tm @@ -166,3 +166,87 @@ def test_append_tz_dateutil(self): appended = rng.append(rng2) tm.assert_index_equal(appended, rng3) + + def test_series_append_aware(self): + rng1 = date_range("1/1/2011 01:00", periods=1, freq="H", tz="US/Eastern") + rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="US/Eastern") + ser1 = Series([1], index=rng1) + ser2 = Series([2], index=rng2) + ts_result = ser1.append(ser2) + + exp_index = DatetimeIndex( + ["2011-01-01 01:00", "2011-01-01 02:00"], tz="US/Eastern" + ) + exp = Series([1, 2], index=exp_index) + tm.assert_series_equal(ts_result, exp) + assert ts_result.index.tz == rng1.tz + + rng1 = date_range("1/1/2011 01:00", periods=1, freq="H", tz="UTC") + rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="UTC") + ser1 = Series([1], index=rng1) + ser2 = Series([2], index=rng2) + ts_result = ser1.append(ser2) + + exp_index = DatetimeIndex(["2011-01-01 01:00", "2011-01-01 02:00"], tz="UTC") + exp = Series([1, 2], index=exp_index) + tm.assert_series_equal(ts_result, exp) + utc = rng1.tz + assert utc == ts_result.index.tz + + # GH#7795 + # different tz coerces to object dtype, not UTC + rng1 = date_range("1/1/2011 01:00", periods=1, freq="H", tz="US/Eastern") + rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="US/Central") + ser1 = Series([1], index=rng1) + ser2 = Series([2], index=rng2) + ts_result = ser1.append(ser2) + exp_index = Index( + [ + Timestamp("1/1/2011 01:00", tz="US/Eastern"), + Timestamp("1/1/2011 02:00", tz="US/Central"), + ] + ) + exp = Series([1, 2], index=exp_index) + tm.assert_series_equal(ts_result, exp) + + def test_series_append_aware_naive(self): + rng1 = date_range("1/1/2011 01:00", periods=1, freq="H") + rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="US/Eastern") + ser1 = Series(np.random.randn(len(rng1)), index=rng1) + ser2 = Series(np.random.randn(len(rng2)), index=rng2) + ts_result = ser1.append(ser2) + + expected = ser1.index.astype(object).append(ser2.index.astype(object)) + assert ts_result.index.equals(expected) + + # mixed + rng1 = date_range("1/1/2011 01:00", periods=1, freq="H") + rng2 = range(100) + ser1 = Series(np.random.randn(len(rng1)), index=rng1) + ser2 = Series(np.random.randn(len(rng2)), index=rng2) + ts_result = ser1.append(ser2) + + expected = ser1.index.astype(object).append(ser2.index) + assert ts_result.index.equals(expected) + + def test_series_append_dst(self): + rng1 = date_range("1/1/2016 01:00", periods=3, freq="H", tz="US/Eastern") + rng2 = date_range("8/1/2016 01:00", periods=3, freq="H", tz="US/Eastern") + ser1 = Series([1, 2, 3], index=rng1) + ser2 = Series([10, 11, 12], index=rng2) + ts_result = ser1.append(ser2) + + exp_index = DatetimeIndex( + [ + "2016-01-01 01:00", + "2016-01-01 02:00", + "2016-01-01 03:00", + "2016-08-01 01:00", + "2016-08-01 02:00", + "2016-08-01 03:00", + ], + tz="US/Eastern", + ) + exp = Series([1, 2, 3, 10, 11, 12], index=exp_index) + tm.assert_series_equal(ts_result, exp) + assert ts_result.index.tz == rng1.tz diff --git a/pandas/tests/series/methods/test_argsort.py b/pandas/tests/series/methods/test_argsort.py index 62273e2d363fb..c7fe6ed19a2eb 100644 --- a/pandas/tests/series/methods/test_argsort.py +++ b/pandas/tests/series/methods/test_argsort.py @@ -27,7 +27,7 @@ def test_argsort(self, datetime_series): assert issubclass(argsorted.dtype.type, np.integer) # GH#2967 (introduced bug in 0.11-dev I think) - s = Series([Timestamp("201301{i:02d}".format(i=i)) for i in range(1, 6)]) + s = Series([Timestamp(f"201301{i:02d}") for i in range(1, 6)]) assert s.dtype == "datetime64[ns]" shifted = s.shift(-1) assert shifted.dtype == "datetime64[ns]" diff --git a/pandas/tests/series/methods/test_asfreq.py b/pandas/tests/series/methods/test_asfreq.py new file mode 100644 index 0000000000000..d94b60384a07c --- /dev/null +++ b/pandas/tests/series/methods/test_asfreq.py @@ -0,0 +1,104 @@ +from datetime import datetime + +import numpy as np +import pytest + +from pandas import DataFrame, DatetimeIndex, Series, date_range, period_range +import pandas._testing as tm + +from pandas.tseries.offsets import BDay, BMonthEnd + + +class TestAsFreq: + # TODO: de-duplicate/parametrize or move DataFrame test + def test_asfreq_ts(self): + index = period_range(freq="A", start="1/1/2001", end="12/31/2010") + ts = Series(np.random.randn(len(index)), index=index) + df = DataFrame(np.random.randn(len(index), 3), index=index) + + result = ts.asfreq("D", how="end") + df_result = df.asfreq("D", how="end") + exp_index = index.asfreq("D", how="end") + assert len(result) == len(ts) + tm.assert_index_equal(result.index, exp_index) + tm.assert_index_equal(df_result.index, exp_index) + + result = ts.asfreq("D", how="start") + assert len(result) == len(ts) + tm.assert_index_equal(result.index, index.asfreq("D", how="start")) + + @pytest.mark.parametrize("tz", ["US/Eastern", "dateutil/US/Eastern"]) + def test_tz_aware_asfreq(self, tz): + dr = date_range("2011-12-01", "2012-07-20", freq="D", tz=tz) + + ser = Series(np.random.randn(len(dr)), index=dr) + + # it works! + ser.asfreq("T") + + def test_asfreq(self): + ts = Series( + [0.0, 1.0, 2.0], + index=[ + datetime(2009, 10, 30), + datetime(2009, 11, 30), + datetime(2009, 12, 31), + ], + ) + + daily_ts = ts.asfreq("B") + monthly_ts = daily_ts.asfreq("BM") + tm.assert_series_equal(monthly_ts, ts) + + daily_ts = ts.asfreq("B", method="pad") + monthly_ts = daily_ts.asfreq("BM") + tm.assert_series_equal(monthly_ts, ts) + + daily_ts = ts.asfreq(BDay()) + monthly_ts = daily_ts.asfreq(BMonthEnd()) + tm.assert_series_equal(monthly_ts, ts) + + result = ts[:0].asfreq("M") + assert len(result) == 0 + assert result is not ts + + daily_ts = ts.asfreq("D", fill_value=-1) + result = daily_ts.value_counts().sort_index() + expected = Series([60, 1, 1, 1], index=[-1.0, 2.0, 1.0, 0.0]).sort_index() + tm.assert_series_equal(result, expected) + + def test_asfreq_datetimeindex_empty_series(self): + # GH#14320 + index = DatetimeIndex(["2016-09-29 11:00"]) + expected = Series(index=index, dtype=object).asfreq("H") + result = Series([3], index=index.copy()).asfreq("H") + tm.assert_index_equal(expected.index, result.index) + + def test_asfreq_keep_index_name(self): + # GH#9854 + index_name = "bar" + index = date_range("20130101", periods=20, name=index_name) + df = DataFrame(list(range(20)), columns=["foo"], index=index) + + assert index_name == df.index.name + assert index_name == df.asfreq("10D").index.name + + def test_asfreq_normalize(self): + rng = date_range("1/1/2000 09:30", periods=20) + norm = date_range("1/1/2000", periods=20) + vals = np.random.randn(20) + ts = Series(vals, index=rng) + + result = ts.asfreq("D", normalize=True) + norm = date_range("1/1/2000", periods=20) + expected = Series(vals, index=norm) + + tm.assert_series_equal(result, expected) + + vals = np.random.randn(20, 3) + ts = DataFrame(vals, index=rng) + + result = ts.asfreq("D", normalize=True) + expected = DataFrame(vals, index=norm) + + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/series/methods/test_at_time.py b/pandas/tests/series/methods/test_at_time.py new file mode 100644 index 0000000000000..d9985cf33776a --- /dev/null +++ b/pandas/tests/series/methods/test_at_time.py @@ -0,0 +1,72 @@ +from datetime import time + +import numpy as np +import pytest + +from pandas._libs.tslibs import timezones + +from pandas import DataFrame, Series, date_range +import pandas._testing as tm + + +class TestAtTime: + @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"]) + def test_localized_at_time(self, tzstr): + tz = timezones.maybe_get_tz(tzstr) + + rng = date_range("4/16/2012", "5/1/2012", freq="H") + ts = Series(np.random.randn(len(rng)), index=rng) + + ts_local = ts.tz_localize(tzstr) + + result = ts_local.at_time(time(10, 0)) + expected = ts.at_time(time(10, 0)).tz_localize(tzstr) + tm.assert_series_equal(result, expected) + assert timezones.tz_compare(result.index.tz, tz) + + def test_at_time(self): + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = Series(np.random.randn(len(rng)), index=rng) + rs = ts.at_time(rng[1]) + assert (rs.index.hour == rng[1].hour).all() + assert (rs.index.minute == rng[1].minute).all() + assert (rs.index.second == rng[1].second).all() + + result = ts.at_time("9:30") + expected = ts.at_time(time(9, 30)) + tm.assert_series_equal(result, expected) + + df = DataFrame(np.random.randn(len(rng), 3), index=rng) + + result = ts[time(9, 30)] + result_df = df.loc[time(9, 30)] + expected = ts[(rng.hour == 9) & (rng.minute == 30)] + exp_df = df[(rng.hour == 9) & (rng.minute == 30)] + + tm.assert_series_equal(result, expected) + tm.assert_frame_equal(result_df, exp_df) + + chunk = df.loc["1/4/2000":] + result = chunk.loc[time(9, 30)] + expected = result_df[-1:] + tm.assert_frame_equal(result, expected) + + # midnight, everything + rng = date_range("1/1/2000", "1/31/2000") + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts.at_time(time(0, 0)) + tm.assert_series_equal(result, ts) + + # time doesn't exist + rng = date_range("1/1/2012", freq="23Min", periods=384) + ts = Series(np.random.randn(len(rng)), rng) + rs = ts.at_time("16:00") + assert len(rs) == 0 + + def test_at_time_raises(self): + # GH20725 + ser = Series("a b c".split()) + msg = "Index must be DatetimeIndex" + with pytest.raises(TypeError, match=msg): + ser.at_time("00:00") diff --git a/pandas/tests/series/methods/test_between.py b/pandas/tests/series/methods/test_between.py new file mode 100644 index 0000000000000..350a3fe6ff009 --- /dev/null +++ b/pandas/tests/series/methods/test_between.py @@ -0,0 +1,35 @@ +import numpy as np + +from pandas import Series, bdate_range, date_range, period_range +import pandas._testing as tm + + +class TestBetween: + + # TODO: redundant with test_between_datetime_values? + def test_between(self): + series = Series(date_range("1/1/2000", periods=10)) + left, right = series[[2, 7]] + + result = series.between(left, right) + expected = (series >= left) & (series <= right) + tm.assert_series_equal(result, expected) + + def test_between_datetime_values(self): + ser = Series(bdate_range("1/1/2000", periods=20).astype(object)) + ser[::2] = np.nan + + result = ser[ser.between(ser[3], ser[17])] + expected = ser[3:18].dropna() + tm.assert_series_equal(result, expected) + + result = ser[ser.between(ser[3], ser[17], inclusive=False)] + expected = ser[5:16].dropna() + tm.assert_series_equal(result, expected) + + def test_between_period_values(self): + ser = Series(period_range("2000-01-01", periods=10, freq="D")) + left, right = ser[[2, 7]] + result = ser.between(left, right) + expected = (ser >= left) & (ser <= right) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/methods/test_between_time.py b/pandas/tests/series/methods/test_between_time.py new file mode 100644 index 0000000000000..3fa26afe77a1d --- /dev/null +++ b/pandas/tests/series/methods/test_between_time.py @@ -0,0 +1,144 @@ +from datetime import datetime, time +from itertools import product + +import numpy as np +import pytest + +from pandas._libs.tslibs import timezones +import pandas.util._test_decorators as td + +from pandas import DataFrame, Series, date_range +import pandas._testing as tm + + +class TestBetweenTime: + @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"]) + def test_localized_between_time(self, tzstr): + tz = timezones.maybe_get_tz(tzstr) + + rng = date_range("4/16/2012", "5/1/2012", freq="H") + ts = Series(np.random.randn(len(rng)), index=rng) + + ts_local = ts.tz_localize(tzstr) + + t1, t2 = time(10, 0), time(11, 0) + result = ts_local.between_time(t1, t2) + expected = ts.between_time(t1, t2).tz_localize(tzstr) + tm.assert_series_equal(result, expected) + assert timezones.tz_compare(result.index.tz, tz) + + def test_between_time(self): + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = Series(np.random.randn(len(rng)), index=rng) + stime = time(0, 0) + etime = time(1, 0) + + close_open = product([True, False], [True, False]) + for inc_start, inc_end in close_open: + filtered = ts.between_time(stime, etime, inc_start, inc_end) + exp_len = 13 * 4 + 1 + if not inc_start: + exp_len -= 5 + if not inc_end: + exp_len -= 4 + + assert len(filtered) == exp_len + for rs in filtered.index: + t = rs.time() + if inc_start: + assert t >= stime + else: + assert t > stime + + if inc_end: + assert t <= etime + else: + assert t < etime + + result = ts.between_time("00:00", "01:00") + expected = ts.between_time(stime, etime) + tm.assert_series_equal(result, expected) + + # across midnight + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = Series(np.random.randn(len(rng)), index=rng) + stime = time(22, 0) + etime = time(9, 0) + + close_open = product([True, False], [True, False]) + for inc_start, inc_end in close_open: + filtered = ts.between_time(stime, etime, inc_start, inc_end) + exp_len = (12 * 11 + 1) * 4 + 1 + if not inc_start: + exp_len -= 4 + if not inc_end: + exp_len -= 4 + + assert len(filtered) == exp_len + for rs in filtered.index: + t = rs.time() + if inc_start: + assert (t >= stime) or (t <= etime) + else: + assert (t > stime) or (t <= etime) + + if inc_end: + assert (t <= etime) or (t >= stime) + else: + assert (t < etime) or (t >= stime) + + def test_between_time_raises(self): + # GH20725 + ser = Series("a b c".split()) + msg = "Index must be DatetimeIndex" + with pytest.raises(TypeError, match=msg): + ser.between_time(start_time="00:00", end_time="12:00") + + def test_between_time_types(self): + # GH11818 + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + msg = r"Cannot convert arg \[datetime\.datetime\(2010, 1, 2, 1, 0\)\] to a time" + with pytest.raises(ValueError, match=msg): + rng.indexer_between_time(datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + + frame = DataFrame({"A": 0}, index=rng) + with pytest.raises(ValueError, match=msg): + frame.between_time(datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + + series = Series(0, index=rng) + with pytest.raises(ValueError, match=msg): + series.between_time(datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + + @td.skip_if_has_locale + def test_between_time_formats(self): + # GH11818 + rng = date_range("1/1/2000", "1/5/2000", freq="5min") + ts = DataFrame(np.random.randn(len(rng), 2), index=rng) + + strings = [ + ("2:00", "2:30"), + ("0200", "0230"), + ("2:00am", "2:30am"), + ("0200am", "0230am"), + ("2:00:00", "2:30:00"), + ("020000", "023000"), + ("2:00:00am", "2:30:00am"), + ("020000am", "023000am"), + ] + expected_length = 28 + + for time_string in strings: + assert len(ts.between_time(*time_string)) == expected_length + + def test_between_time_axis(self): + # issue 8839 + rng = date_range("1/1/2000", periods=100, freq="10min") + ts = Series(np.random.randn(len(rng)), index=rng) + stime, etime = ("08:00:00", "09:00:00") + expected_length = 7 + + assert len(ts.between_time(stime, etime)) == expected_length + assert len(ts.between_time(stime, etime, axis=0)) == expected_length + msg = "No axis named 1 for object type " + with pytest.raises(ValueError, match=msg): + ts.between_time(stime, etime, axis=1) diff --git a/pandas/tests/series/methods/test_combine.py b/pandas/tests/series/methods/test_combine.py new file mode 100644 index 0000000000000..75d47e3daa103 --- /dev/null +++ b/pandas/tests/series/methods/test_combine.py @@ -0,0 +1,17 @@ +from pandas import Series +import pandas._testing as tm + + +class TestCombine: + def test_combine_scalar(self): + # GH#21248 + # Note - combine() with another Series is tested elsewhere because + # it is used when testing operators + ser = Series([i * 10 for i in range(5)]) + result = ser.combine(3, lambda x, y: x + y) + expected = Series([i * 10 + 3 for i in range(5)]) + tm.assert_series_equal(result, expected) + + result = ser.combine(22, lambda x, y: min(x, y)) + expected = Series([min(i * 10, 22) for i in range(5)]) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/methods/test_combine_first.py b/pandas/tests/series/methods/test_combine_first.py new file mode 100644 index 0000000000000..1ee55fbe39513 --- /dev/null +++ b/pandas/tests/series/methods/test_combine_first.py @@ -0,0 +1,94 @@ +from datetime import datetime + +import numpy as np + +import pandas as pd +from pandas import Period, Series, date_range, period_range, to_datetime +import pandas._testing as tm + + +class TestCombineFirst: + def test_combine_first_period_datetime(self): + # GH#3367 + didx = date_range(start="1950-01-31", end="1950-07-31", freq="M") + pidx = period_range(start=Period("1950-1"), end=Period("1950-7"), freq="M") + # check to be consistent with DatetimeIndex + for idx in [didx, pidx]: + a = Series([1, np.nan, np.nan, 4, 5, np.nan, 7], index=idx) + b = Series([9, 9, 9, 9, 9, 9, 9], index=idx) + + result = a.combine_first(b) + expected = Series([1, 9, 9, 4, 5, 9, 7], index=idx, dtype=np.float64) + tm.assert_series_equal(result, expected) + + def test_combine_first_name(self, datetime_series): + result = datetime_series.combine_first(datetime_series[:5]) + assert result.name == datetime_series.name + + def test_combine_first(self): + values = tm.makeIntIndex(20).values.astype(float) + series = Series(values, index=tm.makeIntIndex(20)) + + series_copy = series * 2 + series_copy[::2] = np.NaN + + # nothing used from the input + combined = series.combine_first(series_copy) + + tm.assert_series_equal(combined, series) + + # Holes filled from input + combined = series_copy.combine_first(series) + assert np.isfinite(combined).all() + + tm.assert_series_equal(combined[::2], series[::2]) + tm.assert_series_equal(combined[1::2], series_copy[1::2]) + + # mixed types + index = tm.makeStringIndex(20) + floats = Series(tm.randn(20), index=index) + strings = Series(tm.makeStringIndex(10), index=index[::2]) + + combined = strings.combine_first(floats) + + tm.assert_series_equal(strings, combined.loc[index[::2]]) + tm.assert_series_equal(floats[1::2].astype(object), combined.loc[index[1::2]]) + + # corner case + ser = Series([1.0, 2, 3], index=[0, 1, 2]) + empty = Series([], index=[], dtype=object) + result = ser.combine_first(empty) + ser.index = ser.index.astype("O") + tm.assert_series_equal(ser, result) + + def test_combine_first_dt64(self): + + s0 = to_datetime(Series(["2010", np.NaN])) + s1 = to_datetime(Series([np.NaN, "2011"])) + rs = s0.combine_first(s1) + xp = to_datetime(Series(["2010", "2011"])) + tm.assert_series_equal(rs, xp) + + s0 = to_datetime(Series(["2010", np.NaN])) + s1 = Series([np.NaN, "2011"]) + rs = s0.combine_first(s1) + xp = Series([datetime(2010, 1, 1), "2011"]) + tm.assert_series_equal(rs, xp) + + def test_combine_first_dt_tz_values(self, tz_naive_fixture): + ser1 = pd.Series( + pd.DatetimeIndex(["20150101", "20150102", "20150103"], tz=tz_naive_fixture), + name="ser1", + ) + ser2 = pd.Series( + pd.DatetimeIndex(["20160514", "20160515", "20160516"], tz=tz_naive_fixture), + index=[2, 3, 4], + name="ser2", + ) + result = ser1.combine_first(ser2) + exp_vals = pd.DatetimeIndex( + ["20150101", "20150102", "20150103", "20160515", "20160516"], + tz=tz_naive_fixture, + ) + exp = pd.Series(exp_vals, name="ser1") + tm.assert_series_equal(exp, result) diff --git a/pandas/tests/series/methods/test_convert_dtypes.py b/pandas/tests/series/methods/test_convert_dtypes.py index 923b5a94c5f41..17527a09f07a1 100644 --- a/pandas/tests/series/methods/test_convert_dtypes.py +++ b/pandas/tests/series/methods/test_convert_dtypes.py @@ -81,6 +81,18 @@ class TestSeriesConvertDtypes: ), }, ), + ( # GH32117 + ["h", "i", 1], + np.dtype("O"), + { + ( + (True, False), + (True, False), + (True, False), + (True, False), + ): np.dtype("O"), + }, + ), ( [10, np.nan, 20], np.dtype("float"), @@ -144,11 +156,23 @@ class TestSeriesConvertDtypes: [1, 2.0], object, { - ((True, False), (True, False), (True,), (True, False)): "Int64", + ((True,), (True, False), (True,), (True, False)): "Int64", ((True,), (True, False), (False,), (True, False)): np.dtype( "float" ), - ((False,), (True, False), (False,), (True, False)): np.dtype( + ((False,), (True, False), (True, False), (True, False)): np.dtype( + "object" + ), + }, + ), + ( + [1, 2.5], + object, + { + ((True,), (True, False), (True, False), (True, False)): np.dtype( + "float" + ), + ((False,), (True, False), (True, False), (True, False)): np.dtype( "object" ), }, @@ -246,3 +270,12 @@ def test_convert_dtypes(self, data, maindtype, params, answerdict): # Make sure original not changed tm.assert_series_equal(series, copy) + + def test_convert_string_dtype(self): + # https://github.com/pandas-dev/pandas/issues/31731 -> converting columns + # that are already string dtype + df = pd.DataFrame( + {"A": ["a", "b", pd.NA], "B": ["ä", "ö", "ü"]}, dtype="string" + ) + result = df.convert_dtypes() + tm.assert_frame_equal(df, result) diff --git a/pandas/tests/series/methods/test_droplevel.py b/pandas/tests/series/methods/test_droplevel.py new file mode 100644 index 0000000000000..435eb5751de4b --- /dev/null +++ b/pandas/tests/series/methods/test_droplevel.py @@ -0,0 +1,19 @@ +import pytest + +from pandas import MultiIndex, Series +import pandas._testing as tm + + +class TestDropLevel: + def test_droplevel(self): + # GH#20342 + ser = Series([1, 2, 3, 4]) + ser.index = MultiIndex.from_arrays( + [(1, 2, 3, 4), (5, 6, 7, 8)], names=["a", "b"] + ) + expected = ser.reset_index("b", drop=True) + result = ser.droplevel("b", axis="index") + tm.assert_series_equal(result, expected) + # test that droplevel raises ValueError on axis != 0 + with pytest.raises(ValueError): + ser.droplevel(1, axis="columns") diff --git a/pandas/tests/series/methods/test_first_and_last.py b/pandas/tests/series/methods/test_first_and_last.py new file mode 100644 index 0000000000000..7629dc8cda30b --- /dev/null +++ b/pandas/tests/series/methods/test_first_and_last.py @@ -0,0 +1,69 @@ +""" +Note: includes tests for `last` +""" + +import numpy as np +import pytest + +from pandas import Series, date_range +import pandas._testing as tm + + +class TestFirst: + def test_first_subset(self): + rng = date_range("1/1/2000", "1/1/2010", freq="12h") + ts = Series(np.random.randn(len(rng)), index=rng) + result = ts.first("10d") + assert len(result) == 20 + + rng = date_range("1/1/2000", "1/1/2010", freq="D") + ts = Series(np.random.randn(len(rng)), index=rng) + result = ts.first("10d") + assert len(result) == 10 + + result = ts.first("3M") + expected = ts[:"3/31/2000"] + tm.assert_series_equal(result, expected) + + result = ts.first("21D") + expected = ts[:21] + tm.assert_series_equal(result, expected) + + result = ts[:0].first("3M") + tm.assert_series_equal(result, ts[:0]) + + def test_first_raises(self): + # GH#20725 + ser = Series("a b c".split()) + msg = "'first' only supports a DatetimeIndex index" + with pytest.raises(TypeError, match=msg): + ser.first("1D") + + def test_last_subset(self): + rng = date_range("1/1/2000", "1/1/2010", freq="12h") + ts = Series(np.random.randn(len(rng)), index=rng) + result = ts.last("10d") + assert len(result) == 20 + + rng = date_range("1/1/2000", "1/1/2010", freq="D") + ts = Series(np.random.randn(len(rng)), index=rng) + result = ts.last("10d") + assert len(result) == 10 + + result = ts.last("21D") + expected = ts["12/12/2009":] + tm.assert_series_equal(result, expected) + + result = ts.last("21D") + expected = ts[-21:] + tm.assert_series_equal(result, expected) + + result = ts[:0].last("3M") + tm.assert_series_equal(result, ts[:0]) + + def test_last_raises(self): + # GH#20725 + ser = Series("a b c".split()) + msg = "'last' only supports a DatetimeIndex index" + with pytest.raises(TypeError, match=msg): + ser.last("1D") diff --git a/pandas/tests/series/methods/test_interpolate.py b/pandas/tests/series/methods/test_interpolate.py new file mode 100644 index 0000000000000..6844225a81a8f --- /dev/null +++ b/pandas/tests/series/methods/test_interpolate.py @@ -0,0 +1,673 @@ +import numpy as np +import pytest + +import pandas.util._test_decorators as td + +import pandas as pd +from pandas import Index, MultiIndex, Series, date_range, isna +import pandas._testing as tm + + +@pytest.fixture( + params=[ + "linear", + "index", + "values", + "nearest", + "slinear", + "zero", + "quadratic", + "cubic", + "barycentric", + "krogh", + "polynomial", + "spline", + "piecewise_polynomial", + "from_derivatives", + "pchip", + "akima", + ] +) +def nontemporal_method(request): + """ Fixture that returns an (method name, required kwargs) pair. + + This fixture does not include method 'time' as a parameterization; that + method requires a Series with a DatetimeIndex, and is generally tested + separately from these non-temporal methods. + """ + method = request.param + kwargs = dict(order=1) if method in ("spline", "polynomial") else dict() + return method, kwargs + + +@pytest.fixture( + params=[ + "linear", + "slinear", + "zero", + "quadratic", + "cubic", + "barycentric", + "krogh", + "polynomial", + "spline", + "piecewise_polynomial", + "from_derivatives", + "pchip", + "akima", + ] +) +def interp_methods_ind(request): + """ Fixture that returns a (method name, required kwargs) pair to + be tested for various Index types. + + This fixture does not include methods - 'time', 'index', 'nearest', + 'values' as a parameterization + """ + method = request.param + kwargs = dict(order=1) if method in ("spline", "polynomial") else dict() + return method, kwargs + + +class TestSeriesInterpolateData: + def test_interpolate(self, datetime_series, string_series): + ts = Series(np.arange(len(datetime_series), dtype=float), datetime_series.index) + + ts_copy = ts.copy() + ts_copy[5:10] = np.NaN + + linear_interp = ts_copy.interpolate(method="linear") + tm.assert_series_equal(linear_interp, ts) + + ord_ts = Series( + [d.toordinal() for d in datetime_series.index], index=datetime_series.index + ).astype(float) + + ord_ts_copy = ord_ts.copy() + ord_ts_copy[5:10] = np.NaN + + time_interp = ord_ts_copy.interpolate(method="time") + tm.assert_series_equal(time_interp, ord_ts) + + def test_interpolate_time_raises_for_non_timeseries(self): + # When method='time' is used on a non-TimeSeries that contains a null + # value, a ValueError should be raised. + non_ts = Series([0, 1, 2, np.NaN]) + msg = "time-weighted interpolation only works on Series.* with a DatetimeIndex" + with pytest.raises(ValueError, match=msg): + non_ts.interpolate(method="time") + + @td.skip_if_no_scipy + def test_interpolate_pchip(self): + + ser = Series(np.sort(np.random.uniform(size=100))) + + # interpolate at new_index + new_index = ser.index.union( + Index([49.25, 49.5, 49.75, 50.25, 50.5, 50.75]) + ).astype(float) + interp_s = ser.reindex(new_index).interpolate(method="pchip") + # does not blow up, GH5977 + interp_s[49:51] + + @td.skip_if_no_scipy + def test_interpolate_akima(self): + + ser = Series([10, 11, 12, 13]) + + expected = Series( + [11.00, 11.25, 11.50, 11.75, 12.00, 12.25, 12.50, 12.75, 13.00], + index=Index([1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]), + ) + # interpolate at new_index + new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])).astype( + float + ) + interp_s = ser.reindex(new_index).interpolate(method="akima") + tm.assert_series_equal(interp_s[1:3], expected) + + @td.skip_if_no_scipy + def test_interpolate_piecewise_polynomial(self): + ser = Series([10, 11, 12, 13]) + + expected = Series( + [11.00, 11.25, 11.50, 11.75, 12.00, 12.25, 12.50, 12.75, 13.00], + index=Index([1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]), + ) + # interpolate at new_index + new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])).astype( + float + ) + interp_s = ser.reindex(new_index).interpolate(method="piecewise_polynomial") + tm.assert_series_equal(interp_s[1:3], expected) + + @td.skip_if_no_scipy + def test_interpolate_from_derivatives(self): + ser = Series([10, 11, 12, 13]) + + expected = Series( + [11.00, 11.25, 11.50, 11.75, 12.00, 12.25, 12.50, 12.75, 13.00], + index=Index([1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]), + ) + # interpolate at new_index + new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])).astype( + float + ) + interp_s = ser.reindex(new_index).interpolate(method="from_derivatives") + tm.assert_series_equal(interp_s[1:3], expected) + + @pytest.mark.parametrize( + "kwargs", + [ + {}, + pytest.param( + {"method": "polynomial", "order": 1}, marks=td.skip_if_no_scipy + ), + ], + ) + def test_interpolate_corners(self, kwargs): + s = Series([np.nan, np.nan]) + tm.assert_series_equal(s.interpolate(**kwargs), s) + + s = Series([], dtype=object).interpolate() + tm.assert_series_equal(s.interpolate(**kwargs), s) + + def test_interpolate_index_values(self): + s = Series(np.nan, index=np.sort(np.random.rand(30))) + s[::3] = np.random.randn(10) + + vals = s.index.values.astype(float) + + result = s.interpolate(method="index") + + expected = s.copy() + bad = isna(expected.values) + good = ~bad + expected = Series( + np.interp(vals[bad], vals[good], s.values[good]), index=s.index[bad] + ) + + tm.assert_series_equal(result[bad], expected) + + # 'values' is synonymous with 'index' for the method kwarg + other_result = s.interpolate(method="values") + + tm.assert_series_equal(other_result, result) + tm.assert_series_equal(other_result[bad], expected) + + def test_interpolate_non_ts(self): + s = Series([1, 3, np.nan, np.nan, np.nan, 11]) + msg = ( + "time-weighted interpolation only works on Series or DataFrames " + "with a DatetimeIndex" + ) + with pytest.raises(ValueError, match=msg): + s.interpolate(method="time") + + @pytest.mark.parametrize( + "kwargs", + [ + {}, + pytest.param( + {"method": "polynomial", "order": 1}, marks=td.skip_if_no_scipy + ), + ], + ) + def test_nan_interpolate(self, kwargs): + s = Series([0, 1, np.nan, 3]) + result = s.interpolate(**kwargs) + expected = Series([0.0, 1.0, 2.0, 3.0]) + tm.assert_series_equal(result, expected) + + def test_nan_irregular_index(self): + s = Series([1, 2, np.nan, 4], index=[1, 3, 5, 9]) + result = s.interpolate() + expected = Series([1.0, 2.0, 3.0, 4.0], index=[1, 3, 5, 9]) + tm.assert_series_equal(result, expected) + + def test_nan_str_index(self): + s = Series([0, 1, 2, np.nan], index=list("abcd")) + result = s.interpolate() + expected = Series([0.0, 1.0, 2.0, 2.0], index=list("abcd")) + tm.assert_series_equal(result, expected) + + @td.skip_if_no_scipy + def test_interp_quad(self): + sq = Series([1, 4, np.nan, 16], index=[1, 2, 3, 4]) + result = sq.interpolate(method="quadratic") + expected = Series([1.0, 4.0, 9.0, 16.0], index=[1, 2, 3, 4]) + tm.assert_series_equal(result, expected) + + @td.skip_if_no_scipy + def test_interp_scipy_basic(self): + s = Series([1, 3, np.nan, 12, np.nan, 25]) + # slinear + expected = Series([1.0, 3.0, 7.5, 12.0, 18.5, 25.0]) + result = s.interpolate(method="slinear") + tm.assert_series_equal(result, expected) + + result = s.interpolate(method="slinear", downcast="infer") + tm.assert_series_equal(result, expected) + # nearest + expected = Series([1, 3, 3, 12, 12, 25]) + result = s.interpolate(method="nearest") + tm.assert_series_equal(result, expected.astype("float")) + + result = s.interpolate(method="nearest", downcast="infer") + tm.assert_series_equal(result, expected) + # zero + expected = Series([1, 3, 3, 12, 12, 25]) + result = s.interpolate(method="zero") + tm.assert_series_equal(result, expected.astype("float")) + + result = s.interpolate(method="zero", downcast="infer") + tm.assert_series_equal(result, expected) + # quadratic + # GH #15662. + expected = Series([1, 3.0, 6.823529, 12.0, 18.058824, 25.0]) + result = s.interpolate(method="quadratic") + tm.assert_series_equal(result, expected) + + result = s.interpolate(method="quadratic", downcast="infer") + tm.assert_series_equal(result, expected) + # cubic + expected = Series([1.0, 3.0, 6.8, 12.0, 18.2, 25.0]) + result = s.interpolate(method="cubic") + tm.assert_series_equal(result, expected) + + def test_interp_limit(self): + s = Series([1, 3, np.nan, np.nan, np.nan, 11]) + + expected = Series([1.0, 3.0, 5.0, 7.0, np.nan, 11.0]) + result = s.interpolate(method="linear", limit=2) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("limit", [-1, 0]) + def test_interpolate_invalid_nonpositive_limit(self, nontemporal_method, limit): + # GH 9217: make sure limit is greater than zero. + s = pd.Series([1, 2, np.nan, 4]) + method, kwargs = nontemporal_method + with pytest.raises(ValueError, match="Limit must be greater than 0"): + s.interpolate(limit=limit, method=method, **kwargs) + + def test_interpolate_invalid_float_limit(self, nontemporal_method): + # GH 9217: make sure limit is an integer. + s = pd.Series([1, 2, np.nan, 4]) + method, kwargs = nontemporal_method + limit = 2.0 + with pytest.raises(ValueError, match="Limit must be an integer"): + s.interpolate(limit=limit, method=method, **kwargs) + + @pytest.mark.parametrize("invalid_method", [None, "nonexistent_method"]) + def test_interp_invalid_method(self, invalid_method): + s = Series([1, 3, np.nan, 12, np.nan, 25]) + + msg = f"method must be one of.* Got '{invalid_method}' instead" + with pytest.raises(ValueError, match=msg): + s.interpolate(method=invalid_method) + + # When an invalid method and invalid limit (such as -1) are + # provided, the error message reflects the invalid method. + with pytest.raises(ValueError, match=msg): + s.interpolate(method=invalid_method, limit=-1) + + def test_interp_limit_forward(self): + s = Series([1, 3, np.nan, np.nan, np.nan, 11]) + + # Provide 'forward' (the default) explicitly here. + expected = Series([1.0, 3.0, 5.0, 7.0, np.nan, 11.0]) + + result = s.interpolate(method="linear", limit=2, limit_direction="forward") + tm.assert_series_equal(result, expected) + + result = s.interpolate(method="linear", limit=2, limit_direction="FORWARD") + tm.assert_series_equal(result, expected) + + def test_interp_unlimited(self): + # these test are for issue #16282 default Limit=None is unlimited + s = Series([np.nan, 1.0, 3.0, np.nan, np.nan, np.nan, 11.0, np.nan]) + expected = Series([1.0, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 11.0]) + result = s.interpolate(method="linear", limit_direction="both") + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 11.0]) + result = s.interpolate(method="linear", limit_direction="forward") + tm.assert_series_equal(result, expected) + + expected = Series([1.0, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, np.nan]) + result = s.interpolate(method="linear", limit_direction="backward") + tm.assert_series_equal(result, expected) + + def test_interp_limit_bad_direction(self): + s = Series([1, 3, np.nan, np.nan, np.nan, 11]) + + msg = ( + r"Invalid limit_direction: expecting one of \['forward', " + r"'backward', 'both'\], got 'abc'" + ) + with pytest.raises(ValueError, match=msg): + s.interpolate(method="linear", limit=2, limit_direction="abc") + + # raises an error even if no limit is specified. + with pytest.raises(ValueError, match=msg): + s.interpolate(method="linear", limit_direction="abc") + + # limit_area introduced GH #16284 + def test_interp_limit_area(self): + # These tests are for issue #9218 -- fill NaNs in both directions. + s = Series([np.nan, np.nan, 3, np.nan, np.nan, np.nan, 7, np.nan, np.nan]) + + expected = Series([np.nan, np.nan, 3.0, 4.0, 5.0, 6.0, 7.0, np.nan, np.nan]) + result = s.interpolate(method="linear", limit_area="inside") + tm.assert_series_equal(result, expected) + + expected = Series( + [np.nan, np.nan, 3.0, 4.0, np.nan, np.nan, 7.0, np.nan, np.nan] + ) + result = s.interpolate(method="linear", limit_area="inside", limit=1) + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, np.nan, 3.0, 4.0, np.nan, 6.0, 7.0, np.nan, np.nan]) + result = s.interpolate( + method="linear", limit_area="inside", limit_direction="both", limit=1 + ) + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, np.nan, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, 7.0]) + result = s.interpolate(method="linear", limit_area="outside") + tm.assert_series_equal(result, expected) + + expected = Series( + [np.nan, np.nan, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, np.nan] + ) + result = s.interpolate(method="linear", limit_area="outside", limit=1) + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, 3.0, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, np.nan]) + result = s.interpolate( + method="linear", limit_area="outside", limit_direction="both", limit=1 + ) + tm.assert_series_equal(result, expected) + + expected = Series([3.0, 3.0, 3.0, np.nan, np.nan, np.nan, 7.0, np.nan, np.nan]) + result = s.interpolate( + method="linear", limit_area="outside", limit_direction="backward" + ) + tm.assert_series_equal(result, expected) + + # raises an error even if limit type is wrong. + msg = r"Invalid limit_area: expecting one of \['inside', 'outside'\], got abc" + with pytest.raises(ValueError, match=msg): + s.interpolate(method="linear", limit_area="abc") + + def test_interp_limit_direction(self): + # These tests are for issue #9218 -- fill NaNs in both directions. + s = Series([1, 3, np.nan, np.nan, np.nan, 11]) + + expected = Series([1.0, 3.0, np.nan, 7.0, 9.0, 11.0]) + result = s.interpolate(method="linear", limit=2, limit_direction="backward") + tm.assert_series_equal(result, expected) + + expected = Series([1.0, 3.0, 5.0, np.nan, 9.0, 11.0]) + result = s.interpolate(method="linear", limit=1, limit_direction="both") + tm.assert_series_equal(result, expected) + + # Check that this works on a longer series of nans. + s = Series([1, 3, np.nan, np.nan, np.nan, 7, 9, np.nan, np.nan, 12, np.nan]) + + expected = Series([1.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0, 11.0, 12.0, 12.0]) + result = s.interpolate(method="linear", limit=2, limit_direction="both") + tm.assert_series_equal(result, expected) + + expected = Series( + [1.0, 3.0, 4.0, np.nan, 6.0, 7.0, 9.0, 10.0, 11.0, 12.0, 12.0] + ) + result = s.interpolate(method="linear", limit=1, limit_direction="both") + tm.assert_series_equal(result, expected) + + def test_interp_limit_to_ends(self): + # These test are for issue #10420 -- flow back to beginning. + s = Series([np.nan, np.nan, 5, 7, 9, np.nan]) + + expected = Series([5.0, 5.0, 5.0, 7.0, 9.0, np.nan]) + result = s.interpolate(method="linear", limit=2, limit_direction="backward") + tm.assert_series_equal(result, expected) + + expected = Series([5.0, 5.0, 5.0, 7.0, 9.0, 9.0]) + result = s.interpolate(method="linear", limit=2, limit_direction="both") + tm.assert_series_equal(result, expected) + + def test_interp_limit_before_ends(self): + # These test are for issue #11115 -- limit ends properly. + s = Series([np.nan, np.nan, 5, 7, np.nan, np.nan]) + + expected = Series([np.nan, np.nan, 5.0, 7.0, 7.0, np.nan]) + result = s.interpolate(method="linear", limit=1, limit_direction="forward") + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, 5.0, 5.0, 7.0, np.nan, np.nan]) + result = s.interpolate(method="linear", limit=1, limit_direction="backward") + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, 5.0, 5.0, 7.0, 7.0, np.nan]) + result = s.interpolate(method="linear", limit=1, limit_direction="both") + tm.assert_series_equal(result, expected) + + @td.skip_if_no_scipy + def test_interp_all_good(self): + s = Series([1, 2, 3]) + result = s.interpolate(method="polynomial", order=1) + tm.assert_series_equal(result, s) + + # non-scipy + result = s.interpolate() + tm.assert_series_equal(result, s) + + @pytest.mark.parametrize( + "check_scipy", [False, pytest.param(True, marks=td.skip_if_no_scipy)] + ) + def test_interp_multiIndex(self, check_scipy): + idx = MultiIndex.from_tuples([(0, "a"), (1, "b"), (2, "c")]) + s = Series([1, 2, np.nan], index=idx) + + expected = s.copy() + expected.loc[2] = 2 + result = s.interpolate() + tm.assert_series_equal(result, expected) + + msg = "Only `method=linear` interpolation is supported on MultiIndexes" + if check_scipy: + with pytest.raises(ValueError, match=msg): + s.interpolate(method="polynomial", order=1) + + @td.skip_if_no_scipy + def test_interp_nonmono_raise(self): + s = Series([1, np.nan, 3], index=[0, 2, 1]) + msg = "krogh interpolation requires that the index be monotonic" + with pytest.raises(ValueError, match=msg): + s.interpolate(method="krogh") + + @td.skip_if_no_scipy + @pytest.mark.parametrize("method", ["nearest", "pad"]) + def test_interp_datetime64(self, method, tz_naive_fixture): + df = Series( + [1, np.nan, 3], index=date_range("1/1/2000", periods=3, tz=tz_naive_fixture) + ) + result = df.interpolate(method=method) + expected = Series( + [1.0, 1.0, 3.0], + index=date_range("1/1/2000", periods=3, tz=tz_naive_fixture), + ) + tm.assert_series_equal(result, expected) + + def test_interp_pad_datetime64tz_values(self): + # GH#27628 missing.interpolate_2d should handle datetimetz values + dti = pd.date_range("2015-04-05", periods=3, tz="US/Central") + ser = pd.Series(dti) + ser[1] = pd.NaT + result = ser.interpolate(method="pad") + + expected = pd.Series(dti) + expected[1] = expected[0] + tm.assert_series_equal(result, expected) + + def test_interp_limit_no_nans(self): + # GH 7173 + s = pd.Series([1.0, 2.0, 3.0]) + result = s.interpolate(limit=1) + expected = s + tm.assert_series_equal(result, expected) + + @td.skip_if_no_scipy + @pytest.mark.parametrize("method", ["polynomial", "spline"]) + def test_no_order(self, method): + # see GH-10633, GH-24014 + s = Series([0, 1, np.nan, 3]) + msg = "You must specify the order of the spline or polynomial" + with pytest.raises(ValueError, match=msg): + s.interpolate(method=method) + + @td.skip_if_no_scipy + @pytest.mark.parametrize("order", [-1, -1.0, 0, 0.0, np.nan]) + def test_interpolate_spline_invalid_order(self, order): + s = Series([0, 1, np.nan, 3]) + msg = "order needs to be specified and greater than 0" + with pytest.raises(ValueError, match=msg): + s.interpolate(method="spline", order=order) + + @td.skip_if_no_scipy + def test_spline(self): + s = Series([1, 2, np.nan, 4, 5, np.nan, 7]) + result = s.interpolate(method="spline", order=1) + expected = Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) + tm.assert_series_equal(result, expected) + + @td.skip_if_no_scipy + def test_spline_extrapolate(self): + s = Series([1, 2, 3, 4, np.nan, 6, np.nan]) + result3 = s.interpolate(method="spline", order=1, ext=3) + expected3 = Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 6.0]) + tm.assert_series_equal(result3, expected3) + + result1 = s.interpolate(method="spline", order=1, ext=0) + expected1 = Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) + tm.assert_series_equal(result1, expected1) + + @td.skip_if_no_scipy + def test_spline_smooth(self): + s = Series([1, 2, np.nan, 4, 5.1, np.nan, 7]) + assert ( + s.interpolate(method="spline", order=3, s=0)[5] + != s.interpolate(method="spline", order=3)[5] + ) + + @td.skip_if_no_scipy + def test_spline_interpolation(self): + s = Series(np.arange(10) ** 2) + s[np.random.randint(0, 9, 3)] = np.nan + result1 = s.interpolate(method="spline", order=1) + expected1 = s.interpolate(method="spline", order=1) + tm.assert_series_equal(result1, expected1) + + def test_interp_timedelta64(self): + # GH 6424 + df = Series([1, np.nan, 3], index=pd.to_timedelta([1, 2, 3])) + result = df.interpolate(method="time") + expected = Series([1.0, 2.0, 3.0], index=pd.to_timedelta([1, 2, 3])) + tm.assert_series_equal(result, expected) + + # test for non uniform spacing + df = Series([1, np.nan, 3], index=pd.to_timedelta([1, 2, 4])) + result = df.interpolate(method="time") + expected = Series([1.0, 1.666667, 3.0], index=pd.to_timedelta([1, 2, 4])) + tm.assert_series_equal(result, expected) + + def test_series_interpolate_method_values(self): + # GH#1646 + rng = date_range("1/1/2000", "1/20/2000", freq="D") + ts = Series(np.random.randn(len(rng)), index=rng) + + ts[::2] = np.nan + + result = ts.interpolate(method="values") + exp = ts.interpolate() + tm.assert_series_equal(result, exp) + + def test_series_interpolate_intraday(self): + # #1698 + index = pd.date_range("1/1/2012", periods=4, freq="12D") + ts = pd.Series([0, 12, 24, 36], index) + new_index = index.append(index + pd.DateOffset(days=1)).sort_values() + + exp = ts.reindex(new_index).interpolate(method="time") + + index = pd.date_range("1/1/2012", periods=4, freq="12H") + ts = pd.Series([0, 12, 24, 36], index) + new_index = index.append(index + pd.DateOffset(hours=1)).sort_values() + result = ts.reindex(new_index).interpolate(method="time") + + tm.assert_numpy_array_equal(result.values, exp.values) + + @pytest.mark.parametrize( + "ind", + [ + ["a", "b", "c", "d"], + pd.period_range(start="2019-01-01", periods=4), + pd.interval_range(start=0, end=4), + ], + ) + def test_interp_non_timedelta_index(self, interp_methods_ind, ind): + # gh 21662 + df = pd.DataFrame([0, 1, np.nan, 3], index=ind) + + method, kwargs = interp_methods_ind + if method == "pchip": + pytest.importorskip("scipy") + + if method == "linear": + result = df[0].interpolate(**kwargs) + expected = pd.Series([0.0, 1.0, 2.0, 3.0], name=0, index=ind) + tm.assert_series_equal(result, expected) + else: + expected_error = ( + "Index column must be numeric or datetime type when " + f"using {method} method other than linear. " + "Try setting a numeric or datetime index column before " + "interpolating." + ) + with pytest.raises(ValueError, match=expected_error): + df[0].interpolate(method=method, **kwargs) + + def test_interpolate_timedelta_index(self, interp_methods_ind): + """ + Tests for non numerical index types - object, period, timedelta + Note that all methods except time, index, nearest and values + are tested here. + """ + # gh 21662 + ind = pd.timedelta_range(start=1, periods=4) + df = pd.DataFrame([0, 1, np.nan, 3], index=ind) + + method, kwargs = interp_methods_ind + if method == "pchip": + pytest.importorskip("scipy") + + if method in {"linear", "pchip"}: + result = df[0].interpolate(method=method, **kwargs) + expected = pd.Series([0.0, 1.0, 2.0, 3.0], name=0, index=ind) + tm.assert_series_equal(result, expected) + else: + pytest.skip( + "This interpolation method is not supported for Timedelta Index yet." + ) + + @pytest.mark.parametrize( + "ascending, expected_values", + [(True, [1, 2, 3, 9, 10]), (False, [10, 9, 3, 2, 1])], + ) + def test_interpolate_unsorted_index(self, ascending, expected_values): + # GH 21037 + ts = pd.Series(data=[10, 9, np.nan, 2, 1], index=[10, 9, 3, 2, 1]) + result = ts.sort_index(ascending=ascending).interpolate(method="index") + expected = pd.Series(data=expected_values, index=expected_values, dtype=float) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/methods/test_nlargest.py b/pandas/tests/series/methods/test_nlargest.py index a029965c7394f..b1aa09f387a13 100644 --- a/pandas/tests/series/methods/test_nlargest.py +++ b/pandas/tests/series/methods/test_nlargest.py @@ -98,7 +98,7 @@ class TestSeriesNLargestNSmallest: ) def test_nlargest_error(self, r): dt = r.dtype - msg = "Cannot use method 'n(larg|small)est' with dtype {dt}".format(dt=dt) + msg = f"Cannot use method 'n(larg|small)est' with dtype {dt}" args = 2, len(r), 0, -1 methods = r.nlargest, r.nsmallest for method, arg in product(methods, args): diff --git a/pandas/tests/series/methods/test_rename.py b/pandas/tests/series/methods/test_rename.py new file mode 100644 index 0000000000000..60182f509e657 --- /dev/null +++ b/pandas/tests/series/methods/test_rename.py @@ -0,0 +1,91 @@ +from datetime import datetime + +import numpy as np + +from pandas import Index, Series +import pandas._testing as tm + + +class TestRename: + def test_rename(self, datetime_series): + ts = datetime_series + renamer = lambda x: x.strftime("%Y%m%d") + renamed = ts.rename(renamer) + assert renamed.index[0] == renamer(ts.index[0]) + + # dict + rename_dict = dict(zip(ts.index, renamed.index)) + renamed2 = ts.rename(rename_dict) + tm.assert_series_equal(renamed, renamed2) + + # partial dict + s = Series(np.arange(4), index=["a", "b", "c", "d"], dtype="int64") + renamed = s.rename({"b": "foo", "d": "bar"}) + tm.assert_index_equal(renamed.index, Index(["a", "foo", "c", "bar"])) + + # index with name + renamer = Series( + np.arange(4), index=Index(["a", "b", "c", "d"], name="name"), dtype="int64" + ) + renamed = renamer.rename({}) + assert renamed.index.name == renamer.index.name + + def test_rename_by_series(self): + s = Series(range(5), name="foo") + renamer = Series({1: 10, 2: 20}) + result = s.rename(renamer) + expected = Series(range(5), index=[0, 10, 20, 3, 4], name="foo") + tm.assert_series_equal(result, expected) + + def test_rename_set_name(self): + s = Series(range(4), index=list("abcd")) + for name in ["foo", 123, 123.0, datetime(2001, 11, 11), ("foo",)]: + result = s.rename(name) + assert result.name == name + tm.assert_numpy_array_equal(result.index.values, s.index.values) + assert s.name is None + + def test_rename_set_name_inplace(self): + s = Series(range(3), index=list("abc")) + for name in ["foo", 123, 123.0, datetime(2001, 11, 11), ("foo",)]: + s.rename(name, inplace=True) + assert s.name == name + + exp = np.array(["a", "b", "c"], dtype=np.object_) + tm.assert_numpy_array_equal(s.index.values, exp) + + def test_rename_axis_supported(self): + # Supporting axis for compatibility, detailed in GH-18589 + s = Series(range(5)) + s.rename({}, axis=0) + s.rename({}, axis="index") + # FIXME: dont leave commenred-out + # TODO: clean up shared index validation + # with pytest.raises(ValueError, match="No axis named 5"): + # s.rename({}, axis=5) + + def test_rename_inplace(self, datetime_series): + renamer = lambda x: x.strftime("%Y%m%d") + expected = renamer(datetime_series.index[0]) + + datetime_series.rename(renamer, inplace=True) + assert datetime_series.index[0] == expected + + def test_rename_with_custom_indexer(self): + # GH 27814 + class MyIndexer: + pass + + ix = MyIndexer() + s = Series([1, 2, 3]).rename(ix) + assert s.name is ix + + def test_rename_with_custom_indexer_inplace(self): + # GH 27814 + class MyIndexer: + pass + + ix = MyIndexer() + s = Series([1, 2, 3]) + s.rename(ix, inplace=True) + assert s.name is ix diff --git a/pandas/tests/series/methods/test_reset_index.py b/pandas/tests/series/methods/test_reset_index.py new file mode 100644 index 0000000000000..f0c4895ad7c10 --- /dev/null +++ b/pandas/tests/series/methods/test_reset_index.py @@ -0,0 +1,110 @@ +import numpy as np +import pytest + +from pandas import DataFrame, Index, MultiIndex, RangeIndex, Series +import pandas._testing as tm + + +class TestResetIndex: + def test_reset_index(self): + df = tm.makeDataFrame()[:5] + ser = df.stack() + ser.index.names = ["hash", "category"] + + ser.name = "value" + df = ser.reset_index() + assert "value" in df + + df = ser.reset_index(name="value2") + assert "value2" in df + + # check inplace + s = ser.reset_index(drop=True) + s2 = ser + s2.reset_index(drop=True, inplace=True) + tm.assert_series_equal(s, s2) + + # level + index = MultiIndex( + levels=[["bar"], ["one", "two", "three"], [0, 1]], + codes=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1]], + ) + s = Series(np.random.randn(6), index=index) + rs = s.reset_index(level=1) + assert len(rs.columns) == 2 + + rs = s.reset_index(level=[0, 2], drop=True) + tm.assert_index_equal(rs.index, Index(index.get_level_values(1))) + assert isinstance(rs, Series) + + def test_reset_index_name(self): + s = Series([1, 2, 3], index=Index(range(3), name="x")) + assert s.reset_index().index.name is None + assert s.reset_index(drop=True).index.name is None + + def test_reset_index_level(self): + df = DataFrame([[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"]) + + for levels in ["A", "B"], [0, 1]: + # With MultiIndex + s = df.set_index(["A", "B"])["C"] + + result = s.reset_index(level=levels[0]) + tm.assert_frame_equal(result, df.set_index("B")) + + result = s.reset_index(level=levels[:1]) + tm.assert_frame_equal(result, df.set_index("B")) + + result = s.reset_index(level=levels) + tm.assert_frame_equal(result, df) + + result = df.set_index(["A", "B"]).reset_index(level=levels, drop=True) + tm.assert_frame_equal(result, df[["C"]]) + + with pytest.raises(KeyError, match="Level E "): + s.reset_index(level=["A", "E"]) + + # With single-level Index + s = df.set_index("A")["B"] + + result = s.reset_index(level=levels[0]) + tm.assert_frame_equal(result, df[["A", "B"]]) + + result = s.reset_index(level=levels[:1]) + tm.assert_frame_equal(result, df[["A", "B"]]) + + result = s.reset_index(level=levels[0], drop=True) + tm.assert_series_equal(result, df["B"]) + + with pytest.raises(IndexError, match="Too many levels"): + s.reset_index(level=[0, 1, 2]) + + # Check that .reset_index([],drop=True) doesn't fail + result = Series(range(4)).reset_index([], drop=True) + expected = Series(range(4)) + tm.assert_series_equal(result, expected) + + def test_reset_index_range(self): + # GH 12071 + s = Series(range(2), name="A", dtype="int64") + series_result = s.reset_index() + assert isinstance(series_result.index, RangeIndex) + series_expected = DataFrame( + [[0, 0], [1, 1]], columns=["index", "A"], index=RangeIndex(stop=2) + ) + tm.assert_frame_equal(series_result, series_expected) + + def test_reset_index_drop_errors(self): + # GH 20925 + + # KeyError raised for series index when passed level name is missing + s = Series(range(4)) + with pytest.raises(KeyError, match="does not match index name"): + s.reset_index("wrong", drop=True) + with pytest.raises(KeyError, match="does not match index name"): + s.reset_index("wrong") + + # KeyError raised for series when level to be dropped is missing + s = Series(range(4), index=MultiIndex.from_product([[1, 2]] * 2)) + with pytest.raises(KeyError, match="not found"): + s.reset_index("wrong", drop=True) diff --git a/pandas/tests/series/methods/test_to_period.py b/pandas/tests/series/methods/test_to_period.py new file mode 100644 index 0000000000000..28c4aad3edf32 --- /dev/null +++ b/pandas/tests/series/methods/test_to_period.py @@ -0,0 +1,47 @@ +import numpy as np + +from pandas import ( + DataFrame, + DatetimeIndex, + PeriodIndex, + Series, + date_range, + period_range, +) +import pandas._testing as tm + + +class TestToPeriod: + def test_to_period(self): + rng = date_range("1/1/2000", "1/1/2001", freq="D") + ts = Series(np.random.randn(len(rng)), index=rng) + + pts = ts.to_period() + exp = ts.copy() + exp.index = period_range("1/1/2000", "1/1/2001") + tm.assert_series_equal(pts, exp) + + pts = ts.to_period("M") + exp.index = exp.index.asfreq("M") + tm.assert_index_equal(pts.index, exp.index.asfreq("M")) + tm.assert_series_equal(pts, exp) + + # GH#7606 without freq + idx = DatetimeIndex(["2011-01-01", "2011-01-02", "2011-01-03", "2011-01-04"]) + exp_idx = PeriodIndex( + ["2011-01-01", "2011-01-02", "2011-01-03", "2011-01-04"], freq="D" + ) + + s = Series(np.random.randn(4), index=idx) + expected = s.copy() + expected.index = exp_idx + tm.assert_series_equal(s.to_period(), expected) + + df = DataFrame(np.random.randn(4, 4), index=idx, columns=idx) + expected = df.copy() + expected.index = exp_idx + tm.assert_frame_equal(df.to_period(), expected) + + expected = df.copy() + expected.columns = exp_idx + tm.assert_frame_equal(df.to_period(axis=1), expected) diff --git a/pandas/tests/series/methods/test_to_timestamp.py b/pandas/tests/series/methods/test_to_timestamp.py new file mode 100644 index 0000000000000..44caf1f082a4f --- /dev/null +++ b/pandas/tests/series/methods/test_to_timestamp.py @@ -0,0 +1,54 @@ +from datetime import timedelta + +from pandas import Series, Timedelta, date_range, period_range, to_datetime +import pandas._testing as tm + + +class TestToTimestamp: + def test_to_timestamp(self): + index = period_range(freq="A", start="1/1/2001", end="12/1/2009") + series = Series(1, index=index, name="foo") + + exp_index = date_range("1/1/2001", end="12/31/2009", freq="A-DEC") + result = series.to_timestamp(how="end") + exp_index = exp_index + Timedelta(1, "D") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + assert result.name == "foo" + + exp_index = date_range("1/1/2001", end="1/1/2009", freq="AS-JAN") + result = series.to_timestamp(how="start") + tm.assert_index_equal(result.index, exp_index) + + def _get_with_delta(delta, freq="A-DEC"): + return date_range( + to_datetime("1/1/2001") + delta, + to_datetime("12/31/2009") + delta, + freq=freq, + ) + + delta = timedelta(hours=23) + result = series.to_timestamp("H", "end") + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "h") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + + delta = timedelta(hours=23, minutes=59) + result = series.to_timestamp("T", "end") + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "m") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + + result = series.to_timestamp("S", "end") + delta = timedelta(hours=23, minutes=59, seconds=59) + exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, "s") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + + index = period_range(freq="H", start="1/1/2001", end="1/2/2001") + series = Series(1, index=index, name="foo") + + exp_index = date_range("1/1/2001 00:59:59", end="1/2/2001 00:59:59", freq="H") + result = series.to_timestamp(how="end") + exp_index = exp_index + Timedelta(1, "s") - Timedelta(1, "ns") + tm.assert_index_equal(result.index, exp_index) + assert result.name == "foo" diff --git a/pandas/tests/series/methods/test_truncate.py b/pandas/tests/series/methods/test_truncate.py index d4e2890ed8bf0..c97369b349f56 100644 --- a/pandas/tests/series/methods/test_truncate.py +++ b/pandas/tests/series/methods/test_truncate.py @@ -1,7 +1,10 @@ +from datetime import datetime + import numpy as np import pytest import pandas as pd +from pandas import Series, date_range import pandas._testing as tm from pandas.tseries.offsets import BDay @@ -76,3 +79,33 @@ def test_truncate_nonsortedindex(self): with pytest.raises(ValueError, match=msg): ts.sort_values(ascending=False).truncate(before="2011-11", after="2011-12") + + def test_truncate_datetimeindex_tz(self): + # GH 9243 + idx = date_range("4/1/2005", "4/30/2005", freq="D", tz="US/Pacific") + s = Series(range(len(idx)), index=idx) + result = s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4)) + expected = Series([1, 2, 3], index=idx[1:4]) + tm.assert_series_equal(result, expected) + + def test_truncate_periodindex(self): + # GH 17717 + idx1 = pd.PeriodIndex( + [pd.Period("2017-09-02"), pd.Period("2017-09-02"), pd.Period("2017-09-03")] + ) + series1 = pd.Series([1, 2, 3], index=idx1) + result1 = series1.truncate(after="2017-09-02") + + expected_idx1 = pd.PeriodIndex( + [pd.Period("2017-09-02"), pd.Period("2017-09-02")] + ) + tm.assert_series_equal(result1, pd.Series([1, 2], index=expected_idx1)) + + idx2 = pd.PeriodIndex( + [pd.Period("2017-09-03"), pd.Period("2017-09-02"), pd.Period("2017-09-03")] + ) + series2 = pd.Series([1, 2, 3], index=idx2) + result2 = series2.sort_index().truncate(after="2017-09-02") + + expected_idx2 = pd.PeriodIndex([pd.Period("2017-09-02")]) + tm.assert_series_equal(result2, pd.Series([2], index=expected_idx2)) diff --git a/pandas/tests/series/methods/test_tz_convert.py b/pandas/tests/series/methods/test_tz_convert.py new file mode 100644 index 0000000000000..ce348d5323e62 --- /dev/null +++ b/pandas/tests/series/methods/test_tz_convert.py @@ -0,0 +1,29 @@ +import numpy as np +import pytest + +from pandas import DatetimeIndex, Series, date_range +import pandas._testing as tm + + +class TestTZConvert: + def test_series_tz_convert(self): + rng = date_range("1/1/2011", periods=200, freq="D", tz="US/Eastern") + ts = Series(1, index=rng) + + result = ts.tz_convert("Europe/Berlin") + assert result.index.tz.zone == "Europe/Berlin" + + # can't convert tz-naive + rng = date_range("1/1/2011", periods=200, freq="D") + ts = Series(1, index=rng) + + with pytest.raises(TypeError, match="Cannot convert tz-naive"): + ts.tz_convert("US/Eastern") + + def test_series_tz_convert_to_utc(self): + base = DatetimeIndex(["2011-01-01", "2011-01-02", "2011-01-03"], tz="UTC") + idx1 = base.tz_convert("Asia/Tokyo")[:2] + idx2 = base.tz_convert("US/Eastern")[1:] + + res = Series([1, 2], index=idx1) + Series([1, 1], index=idx2) + tm.assert_series_equal(res, Series([np.nan, 3, np.nan], index=base)) diff --git a/pandas/tests/series/methods/test_tz_localize.py b/pandas/tests/series/methods/test_tz_localize.py new file mode 100644 index 0000000000000..44c55edf77c0a --- /dev/null +++ b/pandas/tests/series/methods/test_tz_localize.py @@ -0,0 +1,88 @@ +import pytest +import pytz + +from pandas._libs.tslibs import timezones + +from pandas import DatetimeIndex, NaT, Series, Timestamp, date_range +import pandas._testing as tm + + +class TestTZLocalize: + def test_series_tz_localize(self): + + rng = date_range("1/1/2011", periods=100, freq="H") + ts = Series(1, index=rng) + + result = ts.tz_localize("utc") + assert result.index.tz.zone == "UTC" + + # Can't localize if already tz-aware + rng = date_range("1/1/2011", periods=100, freq="H", tz="utc") + ts = Series(1, index=rng) + + with pytest.raises(TypeError, match="Already tz-aware"): + ts.tz_localize("US/Eastern") + + def test_series_tz_localize_ambiguous_bool(self): + # make sure that we are correctly accepting bool values as ambiguous + + # GH#14402 + ts = Timestamp("2015-11-01 01:00:03") + expected0 = Timestamp("2015-11-01 01:00:03-0500", tz="US/Central") + expected1 = Timestamp("2015-11-01 01:00:03-0600", tz="US/Central") + + ser = Series([ts]) + expected0 = Series([expected0]) + expected1 = Series([expected1]) + + with pytest.raises(pytz.AmbiguousTimeError): + ser.dt.tz_localize("US/Central") + + result = ser.dt.tz_localize("US/Central", ambiguous=True) + tm.assert_series_equal(result, expected0) + + result = ser.dt.tz_localize("US/Central", ambiguous=[True]) + tm.assert_series_equal(result, expected0) + + result = ser.dt.tz_localize("US/Central", ambiguous=False) + tm.assert_series_equal(result, expected1) + + result = ser.dt.tz_localize("US/Central", ambiguous=[False]) + tm.assert_series_equal(result, expected1) + + @pytest.mark.parametrize("tz", ["Europe/Warsaw", "dateutil/Europe/Warsaw"]) + @pytest.mark.parametrize( + "method, exp", + [ + ["shift_forward", "2015-03-29 03:00:00"], + ["NaT", NaT], + ["raise", None], + ["foo", "invalid"], + ], + ) + def test_series_tz_localize_nonexistent(self, tz, method, exp): + # GH 8917 + n = 60 + dti = date_range(start="2015-03-29 02:00:00", periods=n, freq="min") + s = Series(1, dti) + if method == "raise": + with pytest.raises(pytz.NonExistentTimeError): + s.tz_localize(tz, nonexistent=method) + elif exp == "invalid": + with pytest.raises(ValueError): + dti.tz_localize(tz, nonexistent=method) + else: + result = s.tz_localize(tz, nonexistent=method) + expected = Series(1, index=DatetimeIndex([exp] * n, tz=tz)) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"]) + def test_series_tz_localize_empty(self, tzstr): + # GH#2248 + ser = Series(dtype=object) + + ser2 = ser.tz_localize("utc") + assert ser2.index.tz == pytz.utc + + ser2 = ser.tz_localize(tzstr) + timezones.tz_compare(ser2.index.tz, timezones.maybe_get_tz(tzstr)) diff --git a/pandas/tests/series/methods/test_update.py b/pandas/tests/series/methods/test_update.py new file mode 100644 index 0000000000000..b7f5f33294792 --- /dev/null +++ b/pandas/tests/series/methods/test_update.py @@ -0,0 +1,58 @@ +import numpy as np +import pytest + +from pandas import DataFrame, Series +import pandas._testing as tm + + +class TestUpdate: + def test_update(self): + s = Series([1.5, np.nan, 3.0, 4.0, np.nan]) + s2 = Series([np.nan, 3.5, np.nan, 5.0]) + s.update(s2) + + expected = Series([1.5, 3.5, 3.0, 5.0, np.nan]) + tm.assert_series_equal(s, expected) + + # GH 3217 + df = DataFrame([{"a": 1}, {"a": 3, "b": 2}]) + df["c"] = np.nan + + df["c"].update(Series(["foo"], index=[0])) + expected = DataFrame( + [[1, np.nan, "foo"], [3, 2.0, np.nan]], columns=["a", "b", "c"] + ) + tm.assert_frame_equal(df, expected) + + @pytest.mark.parametrize( + "other, dtype, expected", + [ + # other is int + ([61, 63], "int32", Series([10, 61, 12], dtype="int32")), + ([61, 63], "int64", Series([10, 61, 12])), + ([61, 63], float, Series([10.0, 61.0, 12.0])), + ([61, 63], object, Series([10, 61, 12], dtype=object)), + # other is float, but can be cast to int + ([61.0, 63.0], "int32", Series([10, 61, 12], dtype="int32")), + ([61.0, 63.0], "int64", Series([10, 61, 12])), + ([61.0, 63.0], float, Series([10.0, 61.0, 12.0])), + ([61.0, 63.0], object, Series([10, 61.0, 12], dtype=object)), + # others is float, cannot be cast to int + ([61.1, 63.1], "int32", Series([10.0, 61.1, 12.0])), + ([61.1, 63.1], "int64", Series([10.0, 61.1, 12.0])), + ([61.1, 63.1], float, Series([10.0, 61.1, 12.0])), + ([61.1, 63.1], object, Series([10, 61.1, 12], dtype=object)), + # other is object, cannot be cast + ([(61,), (63,)], "int32", Series([10, (61,), 12])), + ([(61,), (63,)], "int64", Series([10, (61,), 12])), + ([(61,), (63,)], float, Series([10.0, (61,), 12.0])), + ([(61,), (63,)], object, Series([10, (61,), 12])), + ], + ) + def test_update_dtypes(self, other, dtype, expected): + + ser = Series([10, 11, 12], dtype=dtype) + other = Series(other, index=[1, 3]) + ser.update(other) + + tm.assert_series_equal(ser, expected) diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py index 71f6681e8c955..f6ca93b0c2882 100644 --- a/pandas/tests/series/test_alter_axes.py +++ b/pandas/tests/series/test_alter_axes.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from pandas import DataFrame, Index, MultiIndex, RangeIndex, Series +from pandas import Index, MultiIndex, Series import pandas._testing as tm @@ -31,62 +31,6 @@ def test_setindex(self, string_series): # Renaming - def test_rename(self, datetime_series): - ts = datetime_series - renamer = lambda x: x.strftime("%Y%m%d") - renamed = ts.rename(renamer) - assert renamed.index[0] == renamer(ts.index[0]) - - # dict - rename_dict = dict(zip(ts.index, renamed.index)) - renamed2 = ts.rename(rename_dict) - tm.assert_series_equal(renamed, renamed2) - - # partial dict - s = Series(np.arange(4), index=["a", "b", "c", "d"], dtype="int64") - renamed = s.rename({"b": "foo", "d": "bar"}) - tm.assert_index_equal(renamed.index, Index(["a", "foo", "c", "bar"])) - - # index with name - renamer = Series( - np.arange(4), index=Index(["a", "b", "c", "d"], name="name"), dtype="int64" - ) - renamed = renamer.rename({}) - assert renamed.index.name == renamer.index.name - - def test_rename_by_series(self): - s = Series(range(5), name="foo") - renamer = Series({1: 10, 2: 20}) - result = s.rename(renamer) - expected = Series(range(5), index=[0, 10, 20, 3, 4], name="foo") - tm.assert_series_equal(result, expected) - - def test_rename_set_name(self): - s = Series(range(4), index=list("abcd")) - for name in ["foo", 123, 123.0, datetime(2001, 11, 11), ("foo",)]: - result = s.rename(name) - assert result.name == name - tm.assert_numpy_array_equal(result.index.values, s.index.values) - assert s.name is None - - def test_rename_set_name_inplace(self): - s = Series(range(3), index=list("abc")) - for name in ["foo", 123, 123.0, datetime(2001, 11, 11), ("foo",)]: - s.rename(name, inplace=True) - assert s.name == name - - exp = np.array(["a", "b", "c"], dtype=np.object_) - tm.assert_numpy_array_equal(s.index.values, exp) - - def test_rename_axis_supported(self): - # Supporting axis for compatibility, detailed in GH-18589 - s = Series(range(5)) - s.rename({}, axis=0) - s.rename({}, axis="index") - # TODO: clean up shared index validation - # with pytest.raises(ValueError, match="No axis named 5"): - # s.rename({}, axis=5) - def test_set_name_attribute(self): s = Series([1, 2, 3]) s2 = Series([1, 2, 3], name="bar") @@ -103,13 +47,6 @@ def test_set_name(self): assert s.name is None assert s is not s2 - def test_rename_inplace(self, datetime_series): - renamer = lambda x: x.strftime("%Y%m%d") - expected = renamer(datetime_series.index[0]) - - datetime_series.rename(renamer, inplace=True) - assert datetime_series.index[0] == expected - def test_set_index_makes_timeseries(self): idx = tm.makeDateIndex(10) @@ -117,94 +54,6 @@ def test_set_index_makes_timeseries(self): s.index = idx assert s.index.is_all_dates - def test_reset_index(self): - df = tm.makeDataFrame()[:5] - ser = df.stack() - ser.index.names = ["hash", "category"] - - ser.name = "value" - df = ser.reset_index() - assert "value" in df - - df = ser.reset_index(name="value2") - assert "value2" in df - - # check inplace - s = ser.reset_index(drop=True) - s2 = ser - s2.reset_index(drop=True, inplace=True) - tm.assert_series_equal(s, s2) - - # level - index = MultiIndex( - levels=[["bar"], ["one", "two", "three"], [0, 1]], - codes=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1]], - ) - s = Series(np.random.randn(6), index=index) - rs = s.reset_index(level=1) - assert len(rs.columns) == 2 - - rs = s.reset_index(level=[0, 2], drop=True) - tm.assert_index_equal(rs.index, Index(index.get_level_values(1))) - assert isinstance(rs, Series) - - def test_reset_index_name(self): - s = Series([1, 2, 3], index=Index(range(3), name="x")) - assert s.reset_index().index.name is None - assert s.reset_index(drop=True).index.name is None - - def test_reset_index_level(self): - df = DataFrame([[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"]) - - for levels in ["A", "B"], [0, 1]: - # With MultiIndex - s = df.set_index(["A", "B"])["C"] - - result = s.reset_index(level=levels[0]) - tm.assert_frame_equal(result, df.set_index("B")) - - result = s.reset_index(level=levels[:1]) - tm.assert_frame_equal(result, df.set_index("B")) - - result = s.reset_index(level=levels) - tm.assert_frame_equal(result, df) - - result = df.set_index(["A", "B"]).reset_index(level=levels, drop=True) - tm.assert_frame_equal(result, df[["C"]]) - - with pytest.raises(KeyError, match="Level E "): - s.reset_index(level=["A", "E"]) - - # With single-level Index - s = df.set_index("A")["B"] - - result = s.reset_index(level=levels[0]) - tm.assert_frame_equal(result, df[["A", "B"]]) - - result = s.reset_index(level=levels[:1]) - tm.assert_frame_equal(result, df[["A", "B"]]) - - result = s.reset_index(level=levels[0], drop=True) - tm.assert_series_equal(result, df["B"]) - - with pytest.raises(IndexError, match="Too many levels"): - s.reset_index(level=[0, 1, 2]) - - # Check that .reset_index([],drop=True) doesn't fail - result = Series(range(4)).reset_index([], drop=True) - expected = Series(range(4)) - tm.assert_series_equal(result, expected) - - def test_reset_index_range(self): - # GH 12071 - s = Series(range(2), name="A", dtype="int64") - series_result = s.reset_index() - assert isinstance(series_result.index, RangeIndex) - series_expected = DataFrame( - [[0, 0], [1, 1]], columns=["index", "A"], index=RangeIndex(stop=2) - ) - tm.assert_frame_equal(series_result, series_expected) - def test_reorder_levels(self): index = MultiIndex( levels=[["bar"], ["one", "two", "three"], [0, 1]], @@ -268,25 +117,6 @@ def test_rename_axis_none(self, kwargs): expected = Series([1, 2, 3], index=expected_index) tm.assert_series_equal(result, expected) - def test_rename_with_custom_indexer(self): - # GH 27814 - class MyIndexer: - pass - - ix = MyIndexer() - s = Series([1, 2, 3]).rename(ix) - assert s.name is ix - - def test_rename_with_custom_indexer_inplace(self): - # GH 27814 - class MyIndexer: - pass - - ix = MyIndexer() - s = Series([1, 2, 3]) - s.rename(ix, inplace=True) - assert s.name is ix - def test_set_axis_inplace_axes(self, axis_series): # GH14636 ser = Series(np.arange(4), index=[1, 3, 5, 7], dtype="int64") @@ -322,31 +152,3 @@ def test_set_axis_inplace(self): for axis in [2, "foo"]: with pytest.raises(ValueError, match="No axis named"): s.set_axis(list("abcd"), axis=axis, inplace=False) - - def test_reset_index_drop_errors(self): - # GH 20925 - - # KeyError raised for series index when passed level name is missing - s = Series(range(4)) - with pytest.raises(KeyError, match="does not match index name"): - s.reset_index("wrong", drop=True) - with pytest.raises(KeyError, match="does not match index name"): - s.reset_index("wrong") - - # KeyError raised for series when level to be dropped is missing - s = Series(range(4), index=MultiIndex.from_product([[1, 2]] * 2)) - with pytest.raises(KeyError, match="not found"): - s.reset_index("wrong", drop=True) - - def test_droplevel(self): - # GH20342 - ser = Series([1, 2, 3, 4]) - ser.index = MultiIndex.from_arrays( - [(1, 2, 3, 4), (5, 6, 7, 8)], names=["a", "b"] - ) - expected = ser.reset_index("b", drop=True) - result = ser.droplevel("b", axis="index") - tm.assert_series_equal(result, expected) - # test that droplevel raises ValueError on axis != 0 - with pytest.raises(ValueError): - ser.droplevel(1, axis="columns") diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py index e6e91b5d4f5f4..6f45b72154805 100644 --- a/pandas/tests/series/test_analytics.py +++ b/pandas/tests/series/test_analytics.py @@ -169,10 +169,10 @@ def test_validate_any_all_out_keepdims_raises(self, kwargs, func): name = func.__name__ msg = ( - r"the '{arg}' parameter is not " - r"supported in the pandas " - r"implementation of {fname}\(\)" - ).format(arg=param, fname=name) + f"the '{param}' parameter is not " + "supported in the pandas " + fr"implementation of {name}\(\)" + ) with pytest.raises(ValueError, match=msg): func(s, **kwargs) diff --git a/pandas/tests/series/test_api.py b/pandas/tests/series/test_api.py index f96d6ddfc357e..3e877cf2fc787 100644 --- a/pandas/tests/series/test_api.py +++ b/pandas/tests/series/test_api.py @@ -85,10 +85,6 @@ def test_binop_maybe_preserve_name(self, datetime_series): result = getattr(s, op)(cp) assert result.name is None - def test_combine_first_name(self, datetime_series): - result = datetime_series.combine_first(datetime_series[:5]) - assert result.name == datetime_series.name - def test_getitem_preserve_name(self, datetime_series): result = datetime_series[datetime_series > 0] assert result.name == datetime_series.name @@ -136,9 +132,7 @@ def test_constructor_subclass_dict(self, dict_subclass): def test_constructor_ordereddict(self): # GH3283 - data = OrderedDict( - ("col{i}".format(i=i), np.random.random()) for i in range(12) - ) + data = OrderedDict((f"col{i}", np.random.random()) for i in range(12)) series = Series(data) expected = Series(list(data.values()), list(data.keys())) @@ -258,7 +252,7 @@ def get_dir(s): tm.makeIntIndex(10), tm.makeFloatIndex(10), Index([True, False]), - Index(["a{}".format(i) for i in range(101)]), + Index([f"a{i}" for i in range(101)]), pd.MultiIndex.from_tuples(zip("ABCD", "EFGH")), pd.MultiIndex.from_tuples(zip([0, 1, 2, 3], "EFGH")), ], diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index f3ffdc373e178..10197766ce4a6 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -2,11 +2,12 @@ import numpy as np import pytest +import pytz from pandas._libs.tslibs import IncompatibleFrequency import pandas as pd -from pandas import Series +from pandas import Series, date_range import pandas._testing as tm @@ -203,3 +204,67 @@ def test_ser_cmp_result_names(self, names, op): ser = Series(cidx).rename(names[1]) result = op(ser, cidx) assert result.name == names[2] + + +# ------------------------------------------------------------------ +# Unsorted +# These arithmetic tests were previously in other files, eventually +# should be parametrized and put into tests.arithmetic + + +class TestTimeSeriesArithmetic: + # TODO: De-duplicate with test below + def test_series_add_tz_mismatch_converts_to_utc_duplicate(self): + rng = date_range("1/1/2011", periods=10, freq="H", tz="US/Eastern") + ser = Series(np.random.randn(len(rng)), index=rng) + + ts_moscow = ser.tz_convert("Europe/Moscow") + + result = ser + ts_moscow + assert result.index.tz is pytz.utc + + result = ts_moscow + ser + assert result.index.tz is pytz.utc + + def test_series_add_tz_mismatch_converts_to_utc(self): + rng = date_range("1/1/2011", periods=100, freq="H", tz="utc") + + perm = np.random.permutation(100)[:90] + ser1 = Series( + np.random.randn(90), index=rng.take(perm).tz_convert("US/Eastern") + ) + + perm = np.random.permutation(100)[:90] + ser2 = Series( + np.random.randn(90), index=rng.take(perm).tz_convert("Europe/Berlin") + ) + + result = ser1 + ser2 + + uts1 = ser1.tz_convert("utc") + uts2 = ser2.tz_convert("utc") + expected = uts1 + uts2 + + assert result.index.tz == pytz.UTC + tm.assert_series_equal(result, expected) + + def test_series_add_aware_naive_raises(self): + rng = date_range("1/1/2011", periods=10, freq="H") + ser = Series(np.random.randn(len(rng)), index=rng) + + ser_utc = ser.tz_localize("utc") + + with pytest.raises(Exception): + ser + ser_utc + + with pytest.raises(Exception): + ser_utc + ser + + def test_datetime_understood(self): + # Ensures it doesn't fail to create the right series + # reported in issue#16726 + series = pd.Series(pd.date_range("2012-01-01", periods=3)) + offset = pd.offsets.DateOffset(days=6) + result = series - offset + expected = pd.Series(pd.to_datetime(["2011-12-26", "2011-12-27", "2011-12-28"])) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_combine_concat.py b/pandas/tests/series/test_combine_concat.py index 4cb471597b67a..adb79f69c2d81 100644 --- a/pandas/tests/series/test_combine_concat.py +++ b/pandas/tests/series/test_combine_concat.py @@ -1,123 +1,28 @@ -from datetime import datetime - import numpy as np import pytest import pandas as pd -from pandas import DataFrame, Series, to_datetime -import pandas._testing as tm +from pandas import Series class TestSeriesCombine: - def test_combine_scalar(self): - # GH 21248 - # Note - combine() with another Series is tested elsewhere because - # it is used when testing operators - s = pd.Series([i * 10 for i in range(5)]) - result = s.combine(3, lambda x, y: x + y) - expected = pd.Series([i * 10 + 3 for i in range(5)]) - tm.assert_series_equal(result, expected) - - result = s.combine(22, lambda x, y: min(x, y)) - expected = pd.Series([min(i * 10, 22) for i in range(5)]) - tm.assert_series_equal(result, expected) - - def test_combine_first(self): - values = tm.makeIntIndex(20).values.astype(float) - series = Series(values, index=tm.makeIntIndex(20)) - - series_copy = series * 2 - series_copy[::2] = np.NaN - - # nothing used from the input - combined = series.combine_first(series_copy) - - tm.assert_series_equal(combined, series) - - # Holes filled from input - combined = series_copy.combine_first(series) - assert np.isfinite(combined).all() - - tm.assert_series_equal(combined[::2], series[::2]) - tm.assert_series_equal(combined[1::2], series_copy[1::2]) - - # mixed types - index = tm.makeStringIndex(20) - floats = Series(tm.randn(20), index=index) - strings = Series(tm.makeStringIndex(10), index=index[::2]) - - combined = strings.combine_first(floats) - - tm.assert_series_equal(strings, combined.loc[index[::2]]) - tm.assert_series_equal(floats[1::2].astype(object), combined.loc[index[1::2]]) - - # corner case - s = Series([1.0, 2, 3], index=[0, 1, 2]) - empty = Series([], index=[], dtype=object) - result = s.combine_first(empty) - s.index = s.index.astype("O") - tm.assert_series_equal(s, result) - - def test_update(self): - s = Series([1.5, np.nan, 3.0, 4.0, np.nan]) - s2 = Series([np.nan, 3.5, np.nan, 5.0]) - s.update(s2) - - expected = Series([1.5, 3.5, 3.0, 5.0, np.nan]) - tm.assert_series_equal(s, expected) - - # GH 3217 - df = DataFrame([{"a": 1}, {"a": 3, "b": 2}]) - df["c"] = np.nan - - df["c"].update(Series(["foo"], index=[0])) - expected = DataFrame( - [[1, np.nan, "foo"], [3, 2.0, np.nan]], columns=["a", "b", "c"] - ) - tm.assert_frame_equal(df, expected) - @pytest.mark.parametrize( - "other, dtype, expected", - [ - # other is int - ([61, 63], "int32", pd.Series([10, 61, 12], dtype="int32")), - ([61, 63], "int64", pd.Series([10, 61, 12])), - ([61, 63], float, pd.Series([10.0, 61.0, 12.0])), - ([61, 63], object, pd.Series([10, 61, 12], dtype=object)), - # other is float, but can be cast to int - ([61.0, 63.0], "int32", pd.Series([10, 61, 12], dtype="int32")), - ([61.0, 63.0], "int64", pd.Series([10, 61, 12])), - ([61.0, 63.0], float, pd.Series([10.0, 61.0, 12.0])), - ([61.0, 63.0], object, pd.Series([10, 61.0, 12], dtype=object)), - # others is float, cannot be cast to int - ([61.1, 63.1], "int32", pd.Series([10.0, 61.1, 12.0])), - ([61.1, 63.1], "int64", pd.Series([10.0, 61.1, 12.0])), - ([61.1, 63.1], float, pd.Series([10.0, 61.1, 12.0])), - ([61.1, 63.1], object, pd.Series([10, 61.1, 12], dtype=object)), - # other is object, cannot be cast - ([(61,), (63,)], "int32", pd.Series([10, (61,), 12])), - ([(61,), (63,)], "int64", pd.Series([10, (61,), 12])), - ([(61,), (63,)], float, pd.Series([10.0, (61,), 12.0])), - ([(61,), (63,)], object, pd.Series([10, (61,), 12])), - ], + "dtype", ["float64", "int8", "uint8", "bool", "m8[ns]", "M8[ns]"] ) - def test_update_dtypes(self, other, dtype, expected): + def test_concat_empty_series_dtypes_match_roundtrips(self, dtype): + dtype = np.dtype(dtype) - s = Series([10, 11, 12], dtype=dtype) - other = Series(other, index=[1, 3]) - s.update(other) + result = pd.concat([Series(dtype=dtype)]) + assert result.dtype == dtype - tm.assert_series_equal(s, expected) + result = pd.concat([Series(dtype=dtype), Series(dtype=dtype)]) + assert result.dtype == dtype def test_concat_empty_series_dtypes_roundtrips(self): # round-tripping with self & like self dtypes = map(np.dtype, ["float64", "int8", "uint8", "bool", "m8[ns]", "M8[ns]"]) - for dtype in dtypes: - assert pd.concat([Series(dtype=dtype)]).dtype == dtype - assert pd.concat([Series(dtype=dtype), Series(dtype=dtype)]).dtype == dtype - def int_result_type(dtype, dtype2): typs = {dtype.kind, dtype2.kind} if not len(typs - {"i", "u", "b"}) and ( @@ -156,53 +61,28 @@ def get_result_type(dtype, dtype2): result = pd.concat([Series(dtype=dtype), Series(dtype=dtype2)]).dtype assert result.kind == expected - def test_combine_first_dt_tz_values(self, tz_naive_fixture): - ser1 = pd.Series( - pd.DatetimeIndex(["20150101", "20150102", "20150103"], tz=tz_naive_fixture), - name="ser1", - ) - ser2 = pd.Series( - pd.DatetimeIndex(["20160514", "20160515", "20160516"], tz=tz_naive_fixture), - index=[2, 3, 4], - name="ser2", - ) - result = ser1.combine_first(ser2) - exp_vals = pd.DatetimeIndex( - ["20150101", "20150102", "20150103", "20160515", "20160516"], - tz=tz_naive_fixture, - ) - exp = pd.Series(exp_vals, name="ser1") - tm.assert_series_equal(exp, result) - - def test_concat_empty_series_dtypes(self): + @pytest.mark.parametrize( + "left,right,expected", + [ + # booleans + (np.bool_, np.int32, np.int32), + (np.bool_, np.float32, np.object_), + # datetime-like + ("m8[ns]", np.bool, np.object_), + ("m8[ns]", np.int64, np.object_), + ("M8[ns]", np.bool, np.object_), + ("M8[ns]", np.int64, np.object_), + # categorical + ("category", "category", "category"), + ("category", "object", "object"), + ], + ) + def test_concat_empty_series_dtypes(self, left, right, expected): + result = pd.concat([Series(dtype=left), Series(dtype=right)]) + assert result.dtype == expected - # booleans - assert ( - pd.concat([Series(dtype=np.bool_), Series(dtype=np.int32)]).dtype - == np.int32 - ) - assert ( - pd.concat([Series(dtype=np.bool_), Series(dtype=np.float32)]).dtype - == np.object_ - ) + def test_concat_empty_series_dtypes_triple(self): - # datetime-like - assert ( - pd.concat([Series(dtype="m8[ns]"), Series(dtype=np.bool)]).dtype - == np.object_ - ) - assert ( - pd.concat([Series(dtype="m8[ns]"), Series(dtype=np.int64)]).dtype - == np.object_ - ) - assert ( - pd.concat([Series(dtype="M8[ns]"), Series(dtype=np.bool)]).dtype - == np.object_ - ) - assert ( - pd.concat([Series(dtype="M8[ns]"), Series(dtype=np.int64)]).dtype - == np.object_ - ) assert ( pd.concat( [Series(dtype="M8[ns]"), Series(dtype=np.bool_), Series(dtype=np.int64)] @@ -210,11 +90,7 @@ def test_concat_empty_series_dtypes(self): == np.object_ ) - # categorical - assert ( - pd.concat([Series(dtype="category"), Series(dtype="category")]).dtype - == "category" - ) + def test_concat_empty_series_dtype_category_with_array(self): # GH 18515 assert ( pd.concat( @@ -222,13 +98,8 @@ def test_concat_empty_series_dtypes(self): ).dtype == "float64" ) - assert ( - pd.concat([Series(dtype="category"), Series(dtype="object")]).dtype - == "object" - ) - # sparse - # TODO: move? + def test_concat_empty_series_dtypes_sparse(self): result = pd.concat( [ Series(dtype="float64").astype("Sparse"), @@ -250,17 +121,3 @@ def test_concat_empty_series_dtypes(self): # TODO: release-note: concat sparse dtype expected = pd.SparseDtype("object") assert result.dtype == expected - - def test_combine_first_dt64(self): - - s0 = to_datetime(Series(["2010", np.NaN])) - s1 = to_datetime(Series([np.NaN, "2011"])) - rs = s0.combine_first(s1) - xp = to_datetime(Series(["2010", "2011"])) - tm.assert_series_equal(rs, xp) - - s0 = to_datetime(Series(["2010", np.NaN])) - s1 = Series([np.NaN, "2011"]) - rs = s0.combine_first(s1) - xp = Series([datetime(2010, 1, 1), "2011"]) - tm.assert_series_equal(rs, xp) diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index 2651c3d73c9ab..1a794f8656abe 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -811,7 +811,7 @@ def test_constructor_dtype_datetime64(self): expected = Series(values2, index=dates) for dtype in ["s", "D", "ms", "us", "ns"]: - values1 = dates.view(np.ndarray).astype("M8[{0}]".format(dtype)) + values1 = dates.view(np.ndarray).astype(f"M8[{dtype}]") result = Series(values1, dates) tm.assert_series_equal(result, expected) @@ -819,7 +819,7 @@ def test_constructor_dtype_datetime64(self): # coerce to non-ns to object properly expected = Series(values2, index=dates, dtype=object) for dtype in ["s", "D", "ms", "us", "ns"]: - values1 = dates.view(np.ndarray).astype("M8[{0}]".format(dtype)) + values1 = dates.view(np.ndarray).astype(f"M8[{dtype}]") result = Series(values1, index=dates, dtype=object) tm.assert_series_equal(result, expected) @@ -952,7 +952,7 @@ def test_constructor_with_datetime_tz(self): def test_construction_to_datetimelike_unit(self, arr_dtype, dtype, unit): # tests all units # gh-19223 - dtype = "{}[{}]".format(dtype, unit) + dtype = f"{dtype}[{unit}]" arr = np.array([1, 2, 3], dtype=arr_dtype) s = Series(arr) result = s.astype(dtype) @@ -1347,12 +1347,11 @@ def test_convert_non_ns(self): def test_constructor_cant_cast_datetimelike(self, index): # floats are not ok - msg = "Cannot cast {}.*? to ".format( - # strip Index to convert PeriodIndex -> Period - # We don't care whether the error message says - # PeriodIndex or PeriodArray - type(index).__name__.rstrip("Index") - ) + # strip Index to convert PeriodIndex -> Period + # We don't care whether the error message says + # PeriodIndex or PeriodArray + msg = f"Cannot cast {type(index).__name__.rstrip('Index')}.*? to " + with pytest.raises(TypeError, match=msg): Series(index, dtype=float) @@ -1422,3 +1421,10 @@ def test_constructor_tz_mixed_data(self): result = Series(dt_list) expected = Series(dt_list, dtype=object) tm.assert_series_equal(result, expected) + + def test_constructor_data_aware_dtype_naive(self, tz_aware_fixture): + # GH#25843 + tz = tz_aware_fixture + result = Series([Timestamp("2019", tz=tz)], dtype="datetime64[ns]") + expected = Series([Timestamp("2019")]) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_cumulative.py b/pandas/tests/series/test_cumulative.py index 885b5bf0476f2..0cb1c038478f5 100644 --- a/pandas/tests/series/test_cumulative.py +++ b/pandas/tests/series/test_cumulative.py @@ -11,6 +11,7 @@ import pytest import pandas as pd +from pandas import _is_numpy_dev import pandas._testing as tm @@ -37,6 +38,11 @@ def test_cumsum(self, datetime_series): def test_cumprod(self, datetime_series): _check_accum_op("cumprod", datetime_series) + @pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, + ) def test_cummin(self, datetime_series): tm.assert_numpy_array_equal( datetime_series.cummin().values, @@ -49,6 +55,11 @@ def test_cummin(self, datetime_series): tm.assert_series_equal(result, expected) + @pytest.mark.xfail( + _is_numpy_dev, + reason="https://github.com/pandas-dev/pandas/issues/31992", + strict=False, + ) def test_cummax(self, datetime_series): tm.assert_numpy_array_equal( datetime_series.cummax().values, diff --git a/pandas/tests/series/test_datetime_values.py b/pandas/tests/series/test_datetime_values.py index b8be4ea137e3d..d22dc72eaaadd 100644 --- a/pandas/tests/series/test_datetime_values.py +++ b/pandas/tests/series/test_datetime_values.py @@ -19,7 +19,6 @@ PeriodIndex, Series, TimedeltaIndex, - bdate_range, date_range, period_range, timedelta_range, @@ -622,18 +621,6 @@ def test_dt_accessor_updates_on_inplace(self): result = s.dt.date assert result[0] == result[2] - def test_between(self): - s = Series(bdate_range("1/1/2000", periods=20).astype(object)) - s[::2] = np.nan - - result = s[s.between(s[3], s[17])] - expected = s[3:18].dropna() - tm.assert_series_equal(result, expected) - - result = s[s.between(s[3], s[17], inclusive=False)] - expected = s[5:16].dropna() - tm.assert_series_equal(result, expected) - def test_date_tz(self): # GH11757 rng = pd.DatetimeIndex( @@ -645,15 +632,6 @@ def test_date_tz(self): tm.assert_series_equal(s.dt.date, expected) tm.assert_series_equal(s.apply(lambda x: x.date()), expected) - def test_datetime_understood(self): - # Ensures it doesn't fail to create the right series - # reported in issue#16726 - series = pd.Series(pd.date_range("2012-01-01", periods=3)) - offset = pd.offsets.DateOffset(days=6) - result = series - offset - expected = pd.Series(pd.to_datetime(["2011-12-26", "2011-12-27", "2011-12-28"])) - tm.assert_series_equal(result, expected) - def test_dt_timetz_accessor(self, tz_naive_fixture): # GH21358 tz = maybe_get_tz(tz_naive_fixture) diff --git a/pandas/tests/series/test_dtypes.py b/pandas/tests/series/test_dtypes.py index 1fc582156a884..80a024eda7848 100644 --- a/pandas/tests/series/test_dtypes.py +++ b/pandas/tests/series/test_dtypes.py @@ -261,7 +261,7 @@ def test_astype_categorical_to_other(self): value = np.random.RandomState(0).randint(0, 10000, 100) df = DataFrame({"value": value}) - labels = ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] + labels = [f"{i} - {i + 499}" for i in range(0, 10000, 500)] cat_labels = Categorical(labels, labels) df = df.sort_values(by=["value"], ascending=True) @@ -384,9 +384,9 @@ def test_astype_generic_timestamp_no_frequency(self, dtype): s = Series(data) msg = ( - r"The '{dtype}' dtype has no unit\. " - r"Please pass in '{dtype}\[ns\]' instead." - ).format(dtype=dtype.__name__) + fr"The '{dtype.__name__}' dtype has no unit\. " + fr"Please pass in '{dtype.__name__}\[ns\]' instead." + ) with pytest.raises(ValueError, match=msg): s.astype(dtype) diff --git a/pandas/tests/series/test_internals.py b/pandas/tests/series/test_internals.py index 4c817ed2e2d59..1566d8f36373b 100644 --- a/pandas/tests/series/test_internals.py +++ b/pandas/tests/series/test_internals.py @@ -169,6 +169,7 @@ def test_convert(self): result = s._convert(datetime=True, coerce=True) tm.assert_series_equal(result, s) + # FIXME: dont leave commented-out # r = s.copy() # r[0] = np.nan # result = r._convert(convert_dates=True,convert_numeric=False) diff --git a/pandas/tests/series/test_missing.py b/pandas/tests/series/test_missing.py index 6b7d9e00a5228..bac005465034f 100644 --- a/pandas/tests/series/test_missing.py +++ b/pandas/tests/series/test_missing.py @@ -5,7 +5,6 @@ import pytz from pandas._libs.tslib import iNaT -import pandas.util._test_decorators as td import pandas as pd from pandas import ( @@ -13,7 +12,6 @@ DataFrame, Index, IntervalIndex, - MultiIndex, NaT, Series, Timedelta, @@ -24,11 +22,6 @@ import pandas._testing as tm -def _simple_ts(start, end, freq="D"): - rng = date_range(start, end, freq=freq) - return Series(np.random.randn(len(rng)), index=rng) - - class TestSeriesMissingData: def test_timedelta_fillna(self): # GH 3371 @@ -988,666 +981,3 @@ def test_series_pad_backfill_limit(self): expected = s[-2:].reindex(index).fillna(method="backfill") expected[:3] = np.nan tm.assert_series_equal(result, expected) - - -@pytest.fixture( - params=[ - "linear", - "index", - "values", - "nearest", - "slinear", - "zero", - "quadratic", - "cubic", - "barycentric", - "krogh", - "polynomial", - "spline", - "piecewise_polynomial", - "from_derivatives", - "pchip", - "akima", - ] -) -def nontemporal_method(request): - """ Fixture that returns an (method name, required kwargs) pair. - - This fixture does not include method 'time' as a parameterization; that - method requires a Series with a DatetimeIndex, and is generally tested - separately from these non-temporal methods. - """ - method = request.param - kwargs = dict(order=1) if method in ("spline", "polynomial") else dict() - return method, kwargs - - -@pytest.fixture( - params=[ - "linear", - "slinear", - "zero", - "quadratic", - "cubic", - "barycentric", - "krogh", - "polynomial", - "spline", - "piecewise_polynomial", - "from_derivatives", - "pchip", - "akima", - ] -) -def interp_methods_ind(request): - """ Fixture that returns a (method name, required kwargs) pair to - be tested for various Index types. - - This fixture does not include methods - 'time', 'index', 'nearest', - 'values' as a parameterization - """ - method = request.param - kwargs = dict(order=1) if method in ("spline", "polynomial") else dict() - return method, kwargs - - -class TestSeriesInterpolateData: - def test_interpolate(self, datetime_series, string_series): - ts = Series(np.arange(len(datetime_series), dtype=float), datetime_series.index) - - ts_copy = ts.copy() - ts_copy[5:10] = np.NaN - - linear_interp = ts_copy.interpolate(method="linear") - tm.assert_series_equal(linear_interp, ts) - - ord_ts = Series( - [d.toordinal() for d in datetime_series.index], index=datetime_series.index - ).astype(float) - - ord_ts_copy = ord_ts.copy() - ord_ts_copy[5:10] = np.NaN - - time_interp = ord_ts_copy.interpolate(method="time") - tm.assert_series_equal(time_interp, ord_ts) - - def test_interpolate_time_raises_for_non_timeseries(self): - # When method='time' is used on a non-TimeSeries that contains a null - # value, a ValueError should be raised. - non_ts = Series([0, 1, 2, np.NaN]) - msg = "time-weighted interpolation only works on Series.* with a DatetimeIndex" - with pytest.raises(ValueError, match=msg): - non_ts.interpolate(method="time") - - @td.skip_if_no_scipy - def test_interpolate_pchip(self): - - ser = Series(np.sort(np.random.uniform(size=100))) - - # interpolate at new_index - new_index = ser.index.union( - Index([49.25, 49.5, 49.75, 50.25, 50.5, 50.75]) - ).astype(float) - interp_s = ser.reindex(new_index).interpolate(method="pchip") - # does not blow up, GH5977 - interp_s[49:51] - - @td.skip_if_no_scipy - def test_interpolate_akima(self): - - ser = Series([10, 11, 12, 13]) - - expected = Series( - [11.00, 11.25, 11.50, 11.75, 12.00, 12.25, 12.50, 12.75, 13.00], - index=Index([1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]), - ) - # interpolate at new_index - new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])).astype( - float - ) - interp_s = ser.reindex(new_index).interpolate(method="akima") - tm.assert_series_equal(interp_s[1:3], expected) - - @td.skip_if_no_scipy - def test_interpolate_piecewise_polynomial(self): - ser = Series([10, 11, 12, 13]) - - expected = Series( - [11.00, 11.25, 11.50, 11.75, 12.00, 12.25, 12.50, 12.75, 13.00], - index=Index([1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]), - ) - # interpolate at new_index - new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])).astype( - float - ) - interp_s = ser.reindex(new_index).interpolate(method="piecewise_polynomial") - tm.assert_series_equal(interp_s[1:3], expected) - - @td.skip_if_no_scipy - def test_interpolate_from_derivatives(self): - ser = Series([10, 11, 12, 13]) - - expected = Series( - [11.00, 11.25, 11.50, 11.75, 12.00, 12.25, 12.50, 12.75, 13.00], - index=Index([1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]), - ) - # interpolate at new_index - new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])).astype( - float - ) - interp_s = ser.reindex(new_index).interpolate(method="from_derivatives") - tm.assert_series_equal(interp_s[1:3], expected) - - @pytest.mark.parametrize( - "kwargs", - [ - {}, - pytest.param( - {"method": "polynomial", "order": 1}, marks=td.skip_if_no_scipy - ), - ], - ) - def test_interpolate_corners(self, kwargs): - s = Series([np.nan, np.nan]) - tm.assert_series_equal(s.interpolate(**kwargs), s) - - s = Series([], dtype=object).interpolate() - tm.assert_series_equal(s.interpolate(**kwargs), s) - - def test_interpolate_index_values(self): - s = Series(np.nan, index=np.sort(np.random.rand(30))) - s[::3] = np.random.randn(10) - - vals = s.index.values.astype(float) - - result = s.interpolate(method="index") - - expected = s.copy() - bad = isna(expected.values) - good = ~bad - expected = Series( - np.interp(vals[bad], vals[good], s.values[good]), index=s.index[bad] - ) - - tm.assert_series_equal(result[bad], expected) - - # 'values' is synonymous with 'index' for the method kwarg - other_result = s.interpolate(method="values") - - tm.assert_series_equal(other_result, result) - tm.assert_series_equal(other_result[bad], expected) - - def test_interpolate_non_ts(self): - s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - msg = ( - "time-weighted interpolation only works on Series or DataFrames " - "with a DatetimeIndex" - ) - with pytest.raises(ValueError, match=msg): - s.interpolate(method="time") - - @pytest.mark.parametrize( - "kwargs", - [ - {}, - pytest.param( - {"method": "polynomial", "order": 1}, marks=td.skip_if_no_scipy - ), - ], - ) - def test_nan_interpolate(self, kwargs): - s = Series([0, 1, np.nan, 3]) - result = s.interpolate(**kwargs) - expected = Series([0.0, 1.0, 2.0, 3.0]) - tm.assert_series_equal(result, expected) - - def test_nan_irregular_index(self): - s = Series([1, 2, np.nan, 4], index=[1, 3, 5, 9]) - result = s.interpolate() - expected = Series([1.0, 2.0, 3.0, 4.0], index=[1, 3, 5, 9]) - tm.assert_series_equal(result, expected) - - def test_nan_str_index(self): - s = Series([0, 1, 2, np.nan], index=list("abcd")) - result = s.interpolate() - expected = Series([0.0, 1.0, 2.0, 2.0], index=list("abcd")) - tm.assert_series_equal(result, expected) - - @td.skip_if_no_scipy - def test_interp_quad(self): - sq = Series([1, 4, np.nan, 16], index=[1, 2, 3, 4]) - result = sq.interpolate(method="quadratic") - expected = Series([1.0, 4.0, 9.0, 16.0], index=[1, 2, 3, 4]) - tm.assert_series_equal(result, expected) - - @td.skip_if_no_scipy - def test_interp_scipy_basic(self): - s = Series([1, 3, np.nan, 12, np.nan, 25]) - # slinear - expected = Series([1.0, 3.0, 7.5, 12.0, 18.5, 25.0]) - result = s.interpolate(method="slinear") - tm.assert_series_equal(result, expected) - - result = s.interpolate(method="slinear", downcast="infer") - tm.assert_series_equal(result, expected) - # nearest - expected = Series([1, 3, 3, 12, 12, 25]) - result = s.interpolate(method="nearest") - tm.assert_series_equal(result, expected.astype("float")) - - result = s.interpolate(method="nearest", downcast="infer") - tm.assert_series_equal(result, expected) - # zero - expected = Series([1, 3, 3, 12, 12, 25]) - result = s.interpolate(method="zero") - tm.assert_series_equal(result, expected.astype("float")) - - result = s.interpolate(method="zero", downcast="infer") - tm.assert_series_equal(result, expected) - # quadratic - # GH #15662. - expected = Series([1, 3.0, 6.823529, 12.0, 18.058824, 25.0]) - result = s.interpolate(method="quadratic") - tm.assert_series_equal(result, expected) - - result = s.interpolate(method="quadratic", downcast="infer") - tm.assert_series_equal(result, expected) - # cubic - expected = Series([1.0, 3.0, 6.8, 12.0, 18.2, 25.0]) - result = s.interpolate(method="cubic") - tm.assert_series_equal(result, expected) - - def test_interp_limit(self): - s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - - expected = Series([1.0, 3.0, 5.0, 7.0, np.nan, 11.0]) - result = s.interpolate(method="linear", limit=2) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize("limit", [-1, 0]) - def test_interpolate_invalid_nonpositive_limit(self, nontemporal_method, limit): - # GH 9217: make sure limit is greater than zero. - s = pd.Series([1, 2, np.nan, 4]) - method, kwargs = nontemporal_method - with pytest.raises(ValueError, match="Limit must be greater than 0"): - s.interpolate(limit=limit, method=method, **kwargs) - - def test_interpolate_invalid_float_limit(self, nontemporal_method): - # GH 9217: make sure limit is an integer. - s = pd.Series([1, 2, np.nan, 4]) - method, kwargs = nontemporal_method - limit = 2.0 - with pytest.raises(ValueError, match="Limit must be an integer"): - s.interpolate(limit=limit, method=method, **kwargs) - - @pytest.mark.parametrize("invalid_method", [None, "nonexistent_method"]) - def test_interp_invalid_method(self, invalid_method): - s = Series([1, 3, np.nan, 12, np.nan, 25]) - - msg = f"method must be one of.* Got '{invalid_method}' instead" - with pytest.raises(ValueError, match=msg): - s.interpolate(method=invalid_method) - - # When an invalid method and invalid limit (such as -1) are - # provided, the error message reflects the invalid method. - with pytest.raises(ValueError, match=msg): - s.interpolate(method=invalid_method, limit=-1) - - def test_interp_limit_forward(self): - s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - - # Provide 'forward' (the default) explicitly here. - expected = Series([1.0, 3.0, 5.0, 7.0, np.nan, 11.0]) - - result = s.interpolate(method="linear", limit=2, limit_direction="forward") - tm.assert_series_equal(result, expected) - - result = s.interpolate(method="linear", limit=2, limit_direction="FORWARD") - tm.assert_series_equal(result, expected) - - def test_interp_unlimited(self): - # these test are for issue #16282 default Limit=None is unlimited - s = Series([np.nan, 1.0, 3.0, np.nan, np.nan, np.nan, 11.0, np.nan]) - expected = Series([1.0, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 11.0]) - result = s.interpolate(method="linear", limit_direction="both") - tm.assert_series_equal(result, expected) - - expected = Series([np.nan, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 11.0]) - result = s.interpolate(method="linear", limit_direction="forward") - tm.assert_series_equal(result, expected) - - expected = Series([1.0, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, np.nan]) - result = s.interpolate(method="linear", limit_direction="backward") - tm.assert_series_equal(result, expected) - - def test_interp_limit_bad_direction(self): - s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - - msg = ( - r"Invalid limit_direction: expecting one of \['forward', " - r"'backward', 'both'\], got 'abc'" - ) - with pytest.raises(ValueError, match=msg): - s.interpolate(method="linear", limit=2, limit_direction="abc") - - # raises an error even if no limit is specified. - with pytest.raises(ValueError, match=msg): - s.interpolate(method="linear", limit_direction="abc") - - # limit_area introduced GH #16284 - def test_interp_limit_area(self): - # These tests are for issue #9218 -- fill NaNs in both directions. - s = Series([np.nan, np.nan, 3, np.nan, np.nan, np.nan, 7, np.nan, np.nan]) - - expected = Series([np.nan, np.nan, 3.0, 4.0, 5.0, 6.0, 7.0, np.nan, np.nan]) - result = s.interpolate(method="linear", limit_area="inside") - tm.assert_series_equal(result, expected) - - expected = Series( - [np.nan, np.nan, 3.0, 4.0, np.nan, np.nan, 7.0, np.nan, np.nan] - ) - result = s.interpolate(method="linear", limit_area="inside", limit=1) - tm.assert_series_equal(result, expected) - - expected = Series([np.nan, np.nan, 3.0, 4.0, np.nan, 6.0, 7.0, np.nan, np.nan]) - result = s.interpolate( - method="linear", limit_area="inside", limit_direction="both", limit=1 - ) - tm.assert_series_equal(result, expected) - - expected = Series([np.nan, np.nan, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, 7.0]) - result = s.interpolate(method="linear", limit_area="outside") - tm.assert_series_equal(result, expected) - - expected = Series( - [np.nan, np.nan, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, np.nan] - ) - result = s.interpolate(method="linear", limit_area="outside", limit=1) - tm.assert_series_equal(result, expected) - - expected = Series([np.nan, 3.0, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, np.nan]) - result = s.interpolate( - method="linear", limit_area="outside", limit_direction="both", limit=1 - ) - tm.assert_series_equal(result, expected) - - expected = Series([3.0, 3.0, 3.0, np.nan, np.nan, np.nan, 7.0, np.nan, np.nan]) - result = s.interpolate( - method="linear", limit_area="outside", limit_direction="backward" - ) - tm.assert_series_equal(result, expected) - - # raises an error even if limit type is wrong. - msg = r"Invalid limit_area: expecting one of \['inside', 'outside'\], got abc" - with pytest.raises(ValueError, match=msg): - s.interpolate(method="linear", limit_area="abc") - - def test_interp_limit_direction(self): - # These tests are for issue #9218 -- fill NaNs in both directions. - s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - - expected = Series([1.0, 3.0, np.nan, 7.0, 9.0, 11.0]) - result = s.interpolate(method="linear", limit=2, limit_direction="backward") - tm.assert_series_equal(result, expected) - - expected = Series([1.0, 3.0, 5.0, np.nan, 9.0, 11.0]) - result = s.interpolate(method="linear", limit=1, limit_direction="both") - tm.assert_series_equal(result, expected) - - # Check that this works on a longer series of nans. - s = Series([1, 3, np.nan, np.nan, np.nan, 7, 9, np.nan, np.nan, 12, np.nan]) - - expected = Series([1.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0, 11.0, 12.0, 12.0]) - result = s.interpolate(method="linear", limit=2, limit_direction="both") - tm.assert_series_equal(result, expected) - - expected = Series( - [1.0, 3.0, 4.0, np.nan, 6.0, 7.0, 9.0, 10.0, 11.0, 12.0, 12.0] - ) - result = s.interpolate(method="linear", limit=1, limit_direction="both") - tm.assert_series_equal(result, expected) - - def test_interp_limit_to_ends(self): - # These test are for issue #10420 -- flow back to beginning. - s = Series([np.nan, np.nan, 5, 7, 9, np.nan]) - - expected = Series([5.0, 5.0, 5.0, 7.0, 9.0, np.nan]) - result = s.interpolate(method="linear", limit=2, limit_direction="backward") - tm.assert_series_equal(result, expected) - - expected = Series([5.0, 5.0, 5.0, 7.0, 9.0, 9.0]) - result = s.interpolate(method="linear", limit=2, limit_direction="both") - tm.assert_series_equal(result, expected) - - def test_interp_limit_before_ends(self): - # These test are for issue #11115 -- limit ends properly. - s = Series([np.nan, np.nan, 5, 7, np.nan, np.nan]) - - expected = Series([np.nan, np.nan, 5.0, 7.0, 7.0, np.nan]) - result = s.interpolate(method="linear", limit=1, limit_direction="forward") - tm.assert_series_equal(result, expected) - - expected = Series([np.nan, 5.0, 5.0, 7.0, np.nan, np.nan]) - result = s.interpolate(method="linear", limit=1, limit_direction="backward") - tm.assert_series_equal(result, expected) - - expected = Series([np.nan, 5.0, 5.0, 7.0, 7.0, np.nan]) - result = s.interpolate(method="linear", limit=1, limit_direction="both") - tm.assert_series_equal(result, expected) - - @td.skip_if_no_scipy - def test_interp_all_good(self): - s = Series([1, 2, 3]) - result = s.interpolate(method="polynomial", order=1) - tm.assert_series_equal(result, s) - - # non-scipy - result = s.interpolate() - tm.assert_series_equal(result, s) - - @pytest.mark.parametrize( - "check_scipy", [False, pytest.param(True, marks=td.skip_if_no_scipy)] - ) - def test_interp_multiIndex(self, check_scipy): - idx = MultiIndex.from_tuples([(0, "a"), (1, "b"), (2, "c")]) - s = Series([1, 2, np.nan], index=idx) - - expected = s.copy() - expected.loc[2] = 2 - result = s.interpolate() - tm.assert_series_equal(result, expected) - - msg = "Only `method=linear` interpolation is supported on MultiIndexes" - if check_scipy: - with pytest.raises(ValueError, match=msg): - s.interpolate(method="polynomial", order=1) - - @td.skip_if_no_scipy - def test_interp_nonmono_raise(self): - s = Series([1, np.nan, 3], index=[0, 2, 1]) - msg = "krogh interpolation requires that the index be monotonic" - with pytest.raises(ValueError, match=msg): - s.interpolate(method="krogh") - - @td.skip_if_no_scipy - @pytest.mark.parametrize("method", ["nearest", "pad"]) - def test_interp_datetime64(self, method, tz_naive_fixture): - df = Series( - [1, np.nan, 3], index=date_range("1/1/2000", periods=3, tz=tz_naive_fixture) - ) - result = df.interpolate(method=method) - expected = Series( - [1.0, 1.0, 3.0], - index=date_range("1/1/2000", periods=3, tz=tz_naive_fixture), - ) - tm.assert_series_equal(result, expected) - - def test_interp_pad_datetime64tz_values(self): - # GH#27628 missing.interpolate_2d should handle datetimetz values - dti = pd.date_range("2015-04-05", periods=3, tz="US/Central") - ser = pd.Series(dti) - ser[1] = pd.NaT - result = ser.interpolate(method="pad") - - expected = pd.Series(dti) - expected[1] = expected[0] - tm.assert_series_equal(result, expected) - - def test_interp_limit_no_nans(self): - # GH 7173 - s = pd.Series([1.0, 2.0, 3.0]) - result = s.interpolate(limit=1) - expected = s - tm.assert_series_equal(result, expected) - - @td.skip_if_no_scipy - @pytest.mark.parametrize("method", ["polynomial", "spline"]) - def test_no_order(self, method): - # see GH-10633, GH-24014 - s = Series([0, 1, np.nan, 3]) - msg = "You must specify the order of the spline or polynomial" - with pytest.raises(ValueError, match=msg): - s.interpolate(method=method) - - @td.skip_if_no_scipy - @pytest.mark.parametrize("order", [-1, -1.0, 0, 0.0, np.nan]) - def test_interpolate_spline_invalid_order(self, order): - s = Series([0, 1, np.nan, 3]) - msg = "order needs to be specified and greater than 0" - with pytest.raises(ValueError, match=msg): - s.interpolate(method="spline", order=order) - - @td.skip_if_no_scipy - def test_spline(self): - s = Series([1, 2, np.nan, 4, 5, np.nan, 7]) - result = s.interpolate(method="spline", order=1) - expected = Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) - tm.assert_series_equal(result, expected) - - @td.skip_if_no_scipy - def test_spline_extrapolate(self): - s = Series([1, 2, 3, 4, np.nan, 6, np.nan]) - result3 = s.interpolate(method="spline", order=1, ext=3) - expected3 = Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 6.0]) - tm.assert_series_equal(result3, expected3) - - result1 = s.interpolate(method="spline", order=1, ext=0) - expected1 = Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) - tm.assert_series_equal(result1, expected1) - - @td.skip_if_no_scipy - def test_spline_smooth(self): - s = Series([1, 2, np.nan, 4, 5.1, np.nan, 7]) - assert ( - s.interpolate(method="spline", order=3, s=0)[5] - != s.interpolate(method="spline", order=3)[5] - ) - - @td.skip_if_no_scipy - def test_spline_interpolation(self): - s = Series(np.arange(10) ** 2) - s[np.random.randint(0, 9, 3)] = np.nan - result1 = s.interpolate(method="spline", order=1) - expected1 = s.interpolate(method="spline", order=1) - tm.assert_series_equal(result1, expected1) - - def test_interp_timedelta64(self): - # GH 6424 - df = Series([1, np.nan, 3], index=pd.to_timedelta([1, 2, 3])) - result = df.interpolate(method="time") - expected = Series([1.0, 2.0, 3.0], index=pd.to_timedelta([1, 2, 3])) - tm.assert_series_equal(result, expected) - - # test for non uniform spacing - df = Series([1, np.nan, 3], index=pd.to_timedelta([1, 2, 4])) - result = df.interpolate(method="time") - expected = Series([1.0, 1.666667, 3.0], index=pd.to_timedelta([1, 2, 4])) - tm.assert_series_equal(result, expected) - - def test_series_interpolate_method_values(self): - # #1646 - ts = _simple_ts("1/1/2000", "1/20/2000") - ts[::2] = np.nan - - result = ts.interpolate(method="values") - exp = ts.interpolate() - tm.assert_series_equal(result, exp) - - def test_series_interpolate_intraday(self): - # #1698 - index = pd.date_range("1/1/2012", periods=4, freq="12D") - ts = pd.Series([0, 12, 24, 36], index) - new_index = index.append(index + pd.DateOffset(days=1)).sort_values() - - exp = ts.reindex(new_index).interpolate(method="time") - - index = pd.date_range("1/1/2012", periods=4, freq="12H") - ts = pd.Series([0, 12, 24, 36], index) - new_index = index.append(index + pd.DateOffset(hours=1)).sort_values() - result = ts.reindex(new_index).interpolate(method="time") - - tm.assert_numpy_array_equal(result.values, exp.values) - - @pytest.mark.parametrize( - "ind", - [ - ["a", "b", "c", "d"], - pd.period_range(start="2019-01-01", periods=4), - pd.interval_range(start=0, end=4), - ], - ) - def test_interp_non_timedelta_index(self, interp_methods_ind, ind): - # gh 21662 - df = pd.DataFrame([0, 1, np.nan, 3], index=ind) - - method, kwargs = interp_methods_ind - if method == "pchip": - pytest.importorskip("scipy") - - if method == "linear": - result = df[0].interpolate(**kwargs) - expected = pd.Series([0.0, 1.0, 2.0, 3.0], name=0, index=ind) - tm.assert_series_equal(result, expected) - else: - expected_error = ( - "Index column must be numeric or datetime type when " - f"using {method} method other than linear. " - "Try setting a numeric or datetime index column before " - "interpolating." - ) - with pytest.raises(ValueError, match=expected_error): - df[0].interpolate(method=method, **kwargs) - - def test_interpolate_timedelta_index(self, interp_methods_ind): - """ - Tests for non numerical index types - object, period, timedelta - Note that all methods except time, index, nearest and values - are tested here. - """ - # gh 21662 - ind = pd.timedelta_range(start=1, periods=4) - df = pd.DataFrame([0, 1, np.nan, 3], index=ind) - - method, kwargs = interp_methods_ind - if method == "pchip": - pytest.importorskip("scipy") - - if method in {"linear", "pchip"}: - result = df[0].interpolate(method=method, **kwargs) - expected = pd.Series([0.0, 1.0, 2.0, 3.0], name=0, index=ind) - tm.assert_series_equal(result, expected) - else: - pytest.skip( - "This interpolation method is not supported for Timedelta Index yet." - ) - - @pytest.mark.parametrize( - "ascending, expected_values", - [(True, [1, 2, 3, 9, 10]), (False, [10, 9, 3, 2, 1])], - ) - def test_interpolate_unsorted_index(self, ascending, expected_values): - # GH 21037 - ts = pd.Series(data=[10, 9, np.nan, 2, 1], index=[10, 9, 3, 2, 1]) - result = ts.sort_index(ascending=ascending).interpolate(method="index") - expected = pd.Series(data=expected_values, index=expected_values, dtype=float) - tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_period.py b/pandas/tests/series/test_period.py index 03fee389542e3..f41245c2872a7 100644 --- a/pandas/tests/series/test_period.py +++ b/pandas/tests/series/test_period.py @@ -52,12 +52,6 @@ def test_dropna(self): s = Series([pd.Period("2011-01", freq="M"), pd.Period("NaT", freq="M")]) tm.assert_series_equal(s.dropna(), Series([pd.Period("2011-01", freq="M")])) - def test_between(self): - left, right = self.series[[2, 7]] - result = self.series.between(left, right) - expected = (self.series >= left) & (self.series <= right) - tm.assert_series_equal(result, expected) - # --------------------------------------------------------------------- # NaT support @@ -110,28 +104,6 @@ def test_align_series(self, join_type): ts.align(ts[::2], join=join_type) - def test_truncate(self): - # GH 17717 - idx1 = pd.PeriodIndex( - [pd.Period("2017-09-02"), pd.Period("2017-09-02"), pd.Period("2017-09-03")] - ) - series1 = pd.Series([1, 2, 3], index=idx1) - result1 = series1.truncate(after="2017-09-02") - - expected_idx1 = pd.PeriodIndex( - [pd.Period("2017-09-02"), pd.Period("2017-09-02")] - ) - tm.assert_series_equal(result1, pd.Series([1, 2], index=expected_idx1)) - - idx2 = pd.PeriodIndex( - [pd.Period("2017-09-03"), pd.Period("2017-09-02"), pd.Period("2017-09-03")] - ) - series2 = pd.Series([1, 2, 3], index=idx2) - result2 = series2.sort_index().truncate(after="2017-09-02") - - expected_idx2 = pd.PeriodIndex([pd.Period("2017-09-02")]) - tm.assert_series_equal(result2, pd.Series([2], index=expected_idx2)) - @pytest.mark.parametrize( "input_vals", [ diff --git a/pandas/tests/series/test_timeseries.py b/pandas/tests/series/test_timeseries.py index 459377fb18f29..c4b2e2edd845a 100644 --- a/pandas/tests/series/test_timeseries.py +++ b/pandas/tests/series/test_timeseries.py @@ -1,30 +1,13 @@ -from datetime import datetime, time, timedelta from io import StringIO -from itertools import product import numpy as np -import pytest from pandas._libs.tslib import iNaT -from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime -import pandas.util._test_decorators as td import pandas as pd -from pandas import ( - DataFrame, - DatetimeIndex, - NaT, - Series, - Timestamp, - concat, - date_range, - timedelta_range, - to_datetime, -) +from pandas import DataFrame, DatetimeIndex, Series, date_range, timedelta_range import pandas._testing as tm -from pandas.tseries.offsets import BDay, BMonthEnd - def _simple_ts(start, end, freq="D"): rng = date_range(start, end, freq=freq) @@ -38,44 +21,6 @@ def assert_range_equal(left, right): class TestTimeSeries: - def test_asfreq(self): - ts = Series( - [0.0, 1.0, 2.0], - index=[ - datetime(2009, 10, 30), - datetime(2009, 11, 30), - datetime(2009, 12, 31), - ], - ) - - daily_ts = ts.asfreq("B") - monthly_ts = daily_ts.asfreq("BM") - tm.assert_series_equal(monthly_ts, ts) - - daily_ts = ts.asfreq("B", method="pad") - monthly_ts = daily_ts.asfreq("BM") - tm.assert_series_equal(monthly_ts, ts) - - daily_ts = ts.asfreq(BDay()) - monthly_ts = daily_ts.asfreq(BMonthEnd()) - tm.assert_series_equal(monthly_ts, ts) - - result = ts[:0].asfreq("M") - assert len(result) == 0 - assert result is not ts - - daily_ts = ts.asfreq("D", fill_value=-1) - result = daily_ts.value_counts().sort_index() - expected = Series([60, 1, 1, 1], index=[-1.0, 2.0, 1.0, 0.0]).sort_index() - tm.assert_series_equal(result, expected) - - def test_asfreq_datetimeindex_empty_series(self): - # GH 14320 - index = pd.DatetimeIndex(["2016-09-29 11:00"]) - expected = Series(index=index, dtype=object).asfreq("H") - result = Series([3], index=index.copy()).asfreq("H") - tm.assert_index_equal(expected.index, result.index) - def test_autocorr(self, datetime_series): # Just run the function corr1 = datetime_series.autocorr() @@ -169,85 +114,6 @@ def test_contiguous_boolean_preserve_freq(self): masked = rng[mask] assert masked.freq is None - def test_to_datetime_unit(self): - - epoch = 1370745748 - s = Series([epoch + t for t in range(20)]) - result = to_datetime(s, unit="s") - expected = Series( - [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] - ) - tm.assert_series_equal(result, expected) - - s = Series([epoch + t for t in range(20)]).astype(float) - result = to_datetime(s, unit="s") - expected = Series( - [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] - ) - tm.assert_series_equal(result, expected) - - s = Series([epoch + t for t in range(20)] + [iNaT]) - result = to_datetime(s, unit="s") - expected = Series( - [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] - + [NaT] - ) - tm.assert_series_equal(result, expected) - - s = Series([epoch + t for t in range(20)] + [iNaT]).astype(float) - result = to_datetime(s, unit="s") - expected = Series( - [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] - + [NaT] - ) - tm.assert_series_equal(result, expected) - - # GH13834 - s = Series([epoch + t for t in np.arange(0, 2, 0.25)] + [iNaT]).astype(float) - result = to_datetime(s, unit="s") - expected = Series( - [ - Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) - for t in np.arange(0, 2, 0.25) - ] - + [NaT] - ) - tm.assert_series_equal(result, expected) - - s = concat( - [Series([epoch + t for t in range(20)]).astype(float), Series([np.nan])], - ignore_index=True, - ) - result = to_datetime(s, unit="s") - expected = Series( - [Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t) for t in range(20)] - + [NaT] - ) - tm.assert_series_equal(result, expected) - - result = to_datetime([1, 2, "NaT", pd.NaT, np.nan], unit="D") - expected = DatetimeIndex( - [Timestamp("1970-01-02"), Timestamp("1970-01-03")] + ["NaT"] * 3 - ) - tm.assert_index_equal(result, expected) - - msg = "non convertible value foo with the unit 'D'" - with pytest.raises(ValueError, match=msg): - to_datetime([1, 2, "foo"], unit="D") - msg = "cannot convert input 111111111 with the unit 'D'" - with pytest.raises(OutOfBoundsDatetime, match=msg): - to_datetime([1, 2, 111111111], unit="D") - - # coerce we can process - expected = DatetimeIndex( - [Timestamp("1970-01-02"), Timestamp("1970-01-03")] + ["NaT"] * 1 - ) - result = to_datetime([1, 2, "foo"], unit="D", errors="coerce") - tm.assert_index_equal(result, expected) - - result = to_datetime([1, 2, 111111111], unit="D", errors="coerce") - tm.assert_index_equal(result, expected) - def test_series_ctor_datetime64(self): rng = date_range("1/1/2000 00:00:00", "1/1/2000 1:59:50", freq="10s") dates = np.asarray(rng) @@ -268,15 +134,6 @@ def test_series_repr_nat(self): ) assert result == expected - def test_asfreq_keep_index_name(self): - # GH #9854 - index_name = "bar" - index = pd.date_range("20130101", periods=20, name=index_name) - df = pd.DataFrame(list(range(20)), columns=["foo"], index=index) - - assert index_name == df.index.name - assert index_name == df.asfreq("10D").index.name - def test_promote_datetime_date(self): rng = date_range("1/1/2000", periods=20) ts = Series(np.random.randn(20), index=rng) @@ -300,295 +157,12 @@ def test_promote_datetime_date(self): expected = rng.get_indexer(ts_slice.index) tm.assert_numpy_array_equal(result, expected) - def test_asfreq_normalize(self): - rng = date_range("1/1/2000 09:30", periods=20) - norm = date_range("1/1/2000", periods=20) - vals = np.random.randn(20) - ts = Series(vals, index=rng) - - result = ts.asfreq("D", normalize=True) - norm = date_range("1/1/2000", periods=20) - expected = Series(vals, index=norm) - - tm.assert_series_equal(result, expected) - - vals = np.random.randn(20, 3) - ts = DataFrame(vals, index=rng) - - result = ts.asfreq("D", normalize=True) - expected = DataFrame(vals, index=norm) - - tm.assert_frame_equal(result, expected) - - def test_first_subset(self): - ts = _simple_ts("1/1/2000", "1/1/2010", freq="12h") - result = ts.first("10d") - assert len(result) == 20 - - ts = _simple_ts("1/1/2000", "1/1/2010") - result = ts.first("10d") - assert len(result) == 10 - - result = ts.first("3M") - expected = ts[:"3/31/2000"] - tm.assert_series_equal(result, expected) - - result = ts.first("21D") - expected = ts[:21] - tm.assert_series_equal(result, expected) - - result = ts[:0].first("3M") - tm.assert_series_equal(result, ts[:0]) - - def test_first_raises(self): - # GH20725 - ser = pd.Series("a b c".split()) - msg = "'first' only supports a DatetimeIndex index" - with pytest.raises(TypeError, match=msg): - ser.first("1D") - - def test_last_subset(self): - ts = _simple_ts("1/1/2000", "1/1/2010", freq="12h") - result = ts.last("10d") - assert len(result) == 20 - - ts = _simple_ts("1/1/2000", "1/1/2010") - result = ts.last("10d") - assert len(result) == 10 - - result = ts.last("21D") - expected = ts["12/12/2009":] - tm.assert_series_equal(result, expected) - - result = ts.last("21D") - expected = ts[-21:] - tm.assert_series_equal(result, expected) - - result = ts[:0].last("3M") - tm.assert_series_equal(result, ts[:0]) - - def test_last_raises(self): - # GH20725 - ser = pd.Series("a b c".split()) - msg = "'last' only supports a DatetimeIndex index" - with pytest.raises(TypeError, match=msg): - ser.last("1D") - def test_format_pre_1900_dates(self): rng = date_range("1/1/1850", "1/1/1950", freq="A-DEC") rng.format() ts = Series(1, index=rng) repr(ts) - def test_at_time(self): - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = Series(np.random.randn(len(rng)), index=rng) - rs = ts.at_time(rng[1]) - assert (rs.index.hour == rng[1].hour).all() - assert (rs.index.minute == rng[1].minute).all() - assert (rs.index.second == rng[1].second).all() - - result = ts.at_time("9:30") - expected = ts.at_time(time(9, 30)) - tm.assert_series_equal(result, expected) - - df = DataFrame(np.random.randn(len(rng), 3), index=rng) - - result = ts[time(9, 30)] - result_df = df.loc[time(9, 30)] - expected = ts[(rng.hour == 9) & (rng.minute == 30)] - exp_df = df[(rng.hour == 9) & (rng.minute == 30)] - - # FIXME: dont leave commented-out - # expected.index = date_range('1/1/2000', '1/4/2000') - - tm.assert_series_equal(result, expected) - tm.assert_frame_equal(result_df, exp_df) - - chunk = df.loc["1/4/2000":] - result = chunk.loc[time(9, 30)] - expected = result_df[-1:] - tm.assert_frame_equal(result, expected) - - # midnight, everything - rng = date_range("1/1/2000", "1/31/2000") - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.at_time(time(0, 0)) - tm.assert_series_equal(result, ts) - - # time doesn't exist - rng = date_range("1/1/2012", freq="23Min", periods=384) - ts = Series(np.random.randn(len(rng)), rng) - rs = ts.at_time("16:00") - assert len(rs) == 0 - - def test_at_time_raises(self): - # GH20725 - ser = pd.Series("a b c".split()) - msg = "Index must be DatetimeIndex" - with pytest.raises(TypeError, match=msg): - ser.at_time("00:00") - - def test_between(self): - series = Series(date_range("1/1/2000", periods=10)) - left, right = series[[2, 7]] - - result = series.between(left, right) - expected = (series >= left) & (series <= right) - tm.assert_series_equal(result, expected) - - def test_between_time(self): - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = Series(np.random.randn(len(rng)), index=rng) - stime = time(0, 0) - etime = time(1, 0) - - close_open = product([True, False], [True, False]) - for inc_start, inc_end in close_open: - filtered = ts.between_time(stime, etime, inc_start, inc_end) - exp_len = 13 * 4 + 1 - if not inc_start: - exp_len -= 5 - if not inc_end: - exp_len -= 4 - - assert len(filtered) == exp_len - for rs in filtered.index: - t = rs.time() - if inc_start: - assert t >= stime - else: - assert t > stime - - if inc_end: - assert t <= etime - else: - assert t < etime - - result = ts.between_time("00:00", "01:00") - expected = ts.between_time(stime, etime) - tm.assert_series_equal(result, expected) - - # across midnight - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = Series(np.random.randn(len(rng)), index=rng) - stime = time(22, 0) - etime = time(9, 0) - - close_open = product([True, False], [True, False]) - for inc_start, inc_end in close_open: - filtered = ts.between_time(stime, etime, inc_start, inc_end) - exp_len = (12 * 11 + 1) * 4 + 1 - if not inc_start: - exp_len -= 4 - if not inc_end: - exp_len -= 4 - - assert len(filtered) == exp_len - for rs in filtered.index: - t = rs.time() - if inc_start: - assert (t >= stime) or (t <= etime) - else: - assert (t > stime) or (t <= etime) - - if inc_end: - assert (t <= etime) or (t >= stime) - else: - assert (t < etime) or (t >= stime) - - def test_between_time_raises(self): - # GH20725 - ser = pd.Series("a b c".split()) - msg = "Index must be DatetimeIndex" - with pytest.raises(TypeError, match=msg): - ser.between_time(start_time="00:00", end_time="12:00") - - def test_between_time_types(self): - # GH11818 - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - msg = r"Cannot convert arg \[datetime\.datetime\(2010, 1, 2, 1, 0\)\] to a time" - with pytest.raises(ValueError, match=msg): - rng.indexer_between_time(datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) - - frame = DataFrame({"A": 0}, index=rng) - with pytest.raises(ValueError, match=msg): - frame.between_time(datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) - - series = Series(0, index=rng) - with pytest.raises(ValueError, match=msg): - series.between_time(datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) - - @td.skip_if_has_locale - def test_between_time_formats(self): - # GH11818 - rng = date_range("1/1/2000", "1/5/2000", freq="5min") - ts = DataFrame(np.random.randn(len(rng), 2), index=rng) - - strings = [ - ("2:00", "2:30"), - ("0200", "0230"), - ("2:00am", "2:30am"), - ("0200am", "0230am"), - ("2:00:00", "2:30:00"), - ("020000", "023000"), - ("2:00:00am", "2:30:00am"), - ("020000am", "023000am"), - ] - expected_length = 28 - - for time_string in strings: - assert len(ts.between_time(*time_string)) == expected_length - - def test_between_time_axis(self): - # issue 8839 - rng = date_range("1/1/2000", periods=100, freq="10min") - ts = Series(np.random.randn(len(rng)), index=rng) - stime, etime = ("08:00:00", "09:00:00") - expected_length = 7 - - assert len(ts.between_time(stime, etime)) == expected_length - assert len(ts.between_time(stime, etime, axis=0)) == expected_length - msg = "No axis named 1 for object type " - with pytest.raises(ValueError, match=msg): - ts.between_time(stime, etime, axis=1) - - def test_to_period(self): - from pandas.core.indexes.period import period_range - - ts = _simple_ts("1/1/2000", "1/1/2001") - - pts = ts.to_period() - exp = ts.copy() - exp.index = period_range("1/1/2000", "1/1/2001") - tm.assert_series_equal(pts, exp) - - pts = ts.to_period("M") - exp.index = exp.index.asfreq("M") - tm.assert_index_equal(pts.index, exp.index.asfreq("M")) - tm.assert_series_equal(pts, exp) - - # GH 7606 without freq - idx = DatetimeIndex(["2011-01-01", "2011-01-02", "2011-01-03", "2011-01-04"]) - exp_idx = pd.PeriodIndex( - ["2011-01-01", "2011-01-02", "2011-01-03", "2011-01-04"], freq="D" - ) - - s = Series(np.random.randn(4), index=idx) - expected = s.copy() - expected.index = exp_idx - tm.assert_series_equal(s.to_period(), expected) - - df = DataFrame(np.random.randn(4, 4), index=idx, columns=idx) - expected = df.copy() - expected.index = exp_idx - tm.assert_frame_equal(df.to_period(), expected) - - expected = df.copy() - expected.columns = exp_idx - tm.assert_frame_equal(df.to_period(axis=1), expected) - def test_groupby_count_dateparseerror(self): dr = date_range(start="1/1/2012", freq="5min", periods=10) @@ -641,82 +215,6 @@ def test_asfreq_resample_set_correct_freq(self): # does .resample() set .freq correctly? assert df.resample("D").asfreq().index.freq == "D" - def test_pickle(self): - - # GH4606 - p = tm.round_trip_pickle(NaT) - assert p is NaT - - idx = pd.to_datetime(["2013-01-01", NaT, "2014-01-06"]) - idx_p = tm.round_trip_pickle(idx) - assert idx_p[0] == idx[0] - assert idx_p[1] is NaT - assert idx_p[2] == idx[2] - - # GH11002 - # don't infer freq - idx = date_range("1750-1-1", "2050-1-1", freq="7D") - idx_p = tm.round_trip_pickle(idx) - tm.assert_index_equal(idx, idx_p) - - @pytest.mark.parametrize("tz", [None, "Asia/Tokyo", "US/Eastern"]) - def test_setops_preserve_freq(self, tz): - rng = date_range("1/1/2000", "1/1/2002", name="idx", tz=tz) - - result = rng[:50].union(rng[50:100]) - assert result.name == rng.name - assert result.freq == rng.freq - assert result.tz == rng.tz - - result = rng[:50].union(rng[30:100]) - assert result.name == rng.name - assert result.freq == rng.freq - assert result.tz == rng.tz - - result = rng[:50].union(rng[60:100]) - assert result.name == rng.name - assert result.freq is None - assert result.tz == rng.tz - - result = rng[:50].intersection(rng[25:75]) - assert result.name == rng.name - assert result.freqstr == "D" - assert result.tz == rng.tz - - nofreq = DatetimeIndex(list(rng[25:75]), name="other") - result = rng[:50].union(nofreq) - assert result.name is None - assert result.freq == rng.freq - assert result.tz == rng.tz - - result = rng[:50].intersection(nofreq) - assert result.name is None - assert result.freq == rng.freq - assert result.tz == rng.tz - - def test_from_M8_structured(self): - dates = [(datetime(2012, 9, 9, 0, 0), datetime(2012, 9, 8, 15, 10))] - arr = np.array(dates, dtype=[("Date", "M8[us]"), ("Forecasting", "M8[us]")]) - df = DataFrame(arr) - - assert df["Date"][0] == dates[0][0] - assert df["Forecasting"][0] == dates[0][1] - - s = Series(arr["Date"]) - assert isinstance(s[0], Timestamp) - assert s[0] == dates[0][0] - - def test_get_level_values_box(self): - from pandas import MultiIndex - - dates = date_range("1/1/2000", periods=4) - levels = [dates, [0, 1]] - codes = [[0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 0, 1, 0, 1, 0, 1]] - - index = MultiIndex(levels=levels, codes=codes) - - assert isinstance(index.get_level_values(0)[0], Timestamp) - def test_view_tz(self): # GH#24024 ser = pd.Series(pd.date_range("2000", periods=4, tz="US/Central")) diff --git a/pandas/tests/series/test_timezones.py b/pandas/tests/series/test_timezones.py index a363f927d10a9..ae4fd12abdb88 100644 --- a/pandas/tests/series/test_timezones.py +++ b/pandas/tests/series/test_timezones.py @@ -10,207 +10,12 @@ from pandas._libs.tslibs import conversion, timezones -from pandas import DatetimeIndex, Index, NaT, Series, Timestamp +from pandas import Series, Timestamp import pandas._testing as tm from pandas.core.indexes.datetimes import date_range class TestSeriesTimezones: - # ----------------------------------------------------------------- - # Series.tz_localize - def test_series_tz_localize(self): - - rng = date_range("1/1/2011", periods=100, freq="H") - ts = Series(1, index=rng) - - result = ts.tz_localize("utc") - assert result.index.tz.zone == "UTC" - - # Can't localize if already tz-aware - rng = date_range("1/1/2011", periods=100, freq="H", tz="utc") - ts = Series(1, index=rng) - - with pytest.raises(TypeError, match="Already tz-aware"): - ts.tz_localize("US/Eastern") - - def test_series_tz_localize_ambiguous_bool(self): - # make sure that we are correctly accepting bool values as ambiguous - - # GH#14402 - ts = Timestamp("2015-11-01 01:00:03") - expected0 = Timestamp("2015-11-01 01:00:03-0500", tz="US/Central") - expected1 = Timestamp("2015-11-01 01:00:03-0600", tz="US/Central") - - ser = Series([ts]) - expected0 = Series([expected0]) - expected1 = Series([expected1]) - - with pytest.raises(pytz.AmbiguousTimeError): - ser.dt.tz_localize("US/Central") - - result = ser.dt.tz_localize("US/Central", ambiguous=True) - tm.assert_series_equal(result, expected0) - - result = ser.dt.tz_localize("US/Central", ambiguous=[True]) - tm.assert_series_equal(result, expected0) - - result = ser.dt.tz_localize("US/Central", ambiguous=False) - tm.assert_series_equal(result, expected1) - - result = ser.dt.tz_localize("US/Central", ambiguous=[False]) - tm.assert_series_equal(result, expected1) - - @pytest.mark.parametrize("tz", ["Europe/Warsaw", "dateutil/Europe/Warsaw"]) - @pytest.mark.parametrize( - "method, exp", - [ - ["shift_forward", "2015-03-29 03:00:00"], - ["NaT", NaT], - ["raise", None], - ["foo", "invalid"], - ], - ) - def test_series_tz_localize_nonexistent(self, tz, method, exp): - # GH 8917 - n = 60 - dti = date_range(start="2015-03-29 02:00:00", periods=n, freq="min") - s = Series(1, dti) - if method == "raise": - with pytest.raises(pytz.NonExistentTimeError): - s.tz_localize(tz, nonexistent=method) - elif exp == "invalid": - with pytest.raises(ValueError): - dti.tz_localize(tz, nonexistent=method) - else: - result = s.tz_localize(tz, nonexistent=method) - expected = Series(1, index=DatetimeIndex([exp] * n, tz=tz)) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"]) - def test_series_tz_localize_empty(self, tzstr): - # GH#2248 - ser = Series(dtype=object) - - ser2 = ser.tz_localize("utc") - assert ser2.index.tz == pytz.utc - - ser2 = ser.tz_localize(tzstr) - timezones.tz_compare(ser2.index.tz, timezones.maybe_get_tz(tzstr)) - - # ----------------------------------------------------------------- - # Series.tz_convert - - def test_series_tz_convert(self): - rng = date_range("1/1/2011", periods=200, freq="D", tz="US/Eastern") - ts = Series(1, index=rng) - - result = ts.tz_convert("Europe/Berlin") - assert result.index.tz.zone == "Europe/Berlin" - - # can't convert tz-naive - rng = date_range("1/1/2011", periods=200, freq="D") - ts = Series(1, index=rng) - - with pytest.raises(TypeError, match="Cannot convert tz-naive"): - ts.tz_convert("US/Eastern") - - def test_series_tz_convert_to_utc(self): - base = DatetimeIndex(["2011-01-01", "2011-01-02", "2011-01-03"], tz="UTC") - idx1 = base.tz_convert("Asia/Tokyo")[:2] - idx2 = base.tz_convert("US/Eastern")[1:] - - res = Series([1, 2], index=idx1) + Series([1, 1], index=idx2) - tm.assert_series_equal(res, Series([np.nan, 3, np.nan], index=base)) - - # ----------------------------------------------------------------- - # Series.append - - def test_series_append_aware(self): - rng1 = date_range("1/1/2011 01:00", periods=1, freq="H", tz="US/Eastern") - rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="US/Eastern") - ser1 = Series([1], index=rng1) - ser2 = Series([2], index=rng2) - ts_result = ser1.append(ser2) - - exp_index = DatetimeIndex( - ["2011-01-01 01:00", "2011-01-01 02:00"], tz="US/Eastern" - ) - exp = Series([1, 2], index=exp_index) - tm.assert_series_equal(ts_result, exp) - assert ts_result.index.tz == rng1.tz - - rng1 = date_range("1/1/2011 01:00", periods=1, freq="H", tz="UTC") - rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="UTC") - ser1 = Series([1], index=rng1) - ser2 = Series([2], index=rng2) - ts_result = ser1.append(ser2) - - exp_index = DatetimeIndex(["2011-01-01 01:00", "2011-01-01 02:00"], tz="UTC") - exp = Series([1, 2], index=exp_index) - tm.assert_series_equal(ts_result, exp) - utc = rng1.tz - assert utc == ts_result.index.tz - - # GH#7795 - # different tz coerces to object dtype, not UTC - rng1 = date_range("1/1/2011 01:00", periods=1, freq="H", tz="US/Eastern") - rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="US/Central") - ser1 = Series([1], index=rng1) - ser2 = Series([2], index=rng2) - ts_result = ser1.append(ser2) - exp_index = Index( - [ - Timestamp("1/1/2011 01:00", tz="US/Eastern"), - Timestamp("1/1/2011 02:00", tz="US/Central"), - ] - ) - exp = Series([1, 2], index=exp_index) - tm.assert_series_equal(ts_result, exp) - - def test_series_append_aware_naive(self): - rng1 = date_range("1/1/2011 01:00", periods=1, freq="H") - rng2 = date_range("1/1/2011 02:00", periods=1, freq="H", tz="US/Eastern") - ser1 = Series(np.random.randn(len(rng1)), index=rng1) - ser2 = Series(np.random.randn(len(rng2)), index=rng2) - ts_result = ser1.append(ser2) - - expected = ser1.index.astype(object).append(ser2.index.astype(object)) - assert ts_result.index.equals(expected) - - # mixed - rng1 = date_range("1/1/2011 01:00", periods=1, freq="H") - rng2 = range(100) - ser1 = Series(np.random.randn(len(rng1)), index=rng1) - ser2 = Series(np.random.randn(len(rng2)), index=rng2) - ts_result = ser1.append(ser2) - - expected = ser1.index.astype(object).append(ser2.index) - assert ts_result.index.equals(expected) - - def test_series_append_dst(self): - rng1 = date_range("1/1/2016 01:00", periods=3, freq="H", tz="US/Eastern") - rng2 = date_range("8/1/2016 01:00", periods=3, freq="H", tz="US/Eastern") - ser1 = Series([1, 2, 3], index=rng1) - ser2 = Series([10, 11, 12], index=rng2) - ts_result = ser1.append(ser2) - - exp_index = DatetimeIndex( - [ - "2016-01-01 01:00", - "2016-01-01 02:00", - "2016-01-01 03:00", - "2016-08-01 01:00", - "2016-08-01 02:00", - "2016-08-01 03:00", - ], - tz="US/Eastern", - ) - exp = Series([1, 2, 3, 10, 11, 12], index=exp_index) - tm.assert_series_equal(ts_result, exp) - assert ts_result.index.tz == rng1.tz - - # ----------------------------------------------------------------- - def test_dateutil_tzoffset_support(self): values = [188.5, 328.25] tzinfo = tzoffset(None, 7200) @@ -225,15 +30,6 @@ def test_dateutil_tzoffset_support(self): # it works! #2443 repr(series.index[0]) - @pytest.mark.parametrize("tz", ["US/Eastern", "dateutil/US/Eastern"]) - def test_tz_aware_asfreq(self, tz): - dr = date_range("2011-12-01", "2012-07-20", freq="D", tz=tz) - - ser = Series(np.random.randn(len(dr)), index=dr) - - # it works! - ser.asfreq("T") - @pytest.mark.parametrize("tz", ["US/Eastern", "dateutil/US/Eastern"]) def test_string_index_alias_tz_aware(self, tz): rng = date_range("1/1/2000", periods=10, tz=tz) @@ -242,53 +38,6 @@ def test_string_index_alias_tz_aware(self, tz): result = ser["1/3/2000"] tm.assert_almost_equal(result, ser[2]) - # TODO: De-duplicate with test below - def test_series_add_tz_mismatch_converts_to_utc_duplicate(self): - rng = date_range("1/1/2011", periods=10, freq="H", tz="US/Eastern") - ser = Series(np.random.randn(len(rng)), index=rng) - - ts_moscow = ser.tz_convert("Europe/Moscow") - - result = ser + ts_moscow - assert result.index.tz is pytz.utc - - result = ts_moscow + ser - assert result.index.tz is pytz.utc - - def test_series_add_tz_mismatch_converts_to_utc(self): - rng = date_range("1/1/2011", periods=100, freq="H", tz="utc") - - perm = np.random.permutation(100)[:90] - ser1 = Series( - np.random.randn(90), index=rng.take(perm).tz_convert("US/Eastern") - ) - - perm = np.random.permutation(100)[:90] - ser2 = Series( - np.random.randn(90), index=rng.take(perm).tz_convert("Europe/Berlin") - ) - - result = ser1 + ser2 - - uts1 = ser1.tz_convert("utc") - uts2 = ser2.tz_convert("utc") - expected = uts1 + uts2 - - assert result.index.tz == pytz.UTC - tm.assert_series_equal(result, expected) - - def test_series_add_aware_naive_raises(self): - rng = date_range("1/1/2011", periods=10, freq="H") - ser = Series(np.random.randn(len(rng)), index=rng) - - ser_utc = ser.tz_localize("utc") - - with pytest.raises(Exception): - ser + ser_utc - - with pytest.raises(Exception): - ser_utc + ser - def test_series_align_aware(self): idx1 = date_range("2001", periods=5, freq="H", tz="US/Eastern") ser = Series(np.random.randn(len(idx1)), index=idx1) @@ -299,28 +48,6 @@ def test_series_align_aware(self): assert new1.index.tz == pytz.UTC assert new2.index.tz == pytz.UTC - @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"]) - def test_localized_at_time_between_time(self, tzstr): - from datetime import time - - tz = timezones.maybe_get_tz(tzstr) - - rng = date_range("4/16/2012", "5/1/2012", freq="H") - ts = Series(np.random.randn(len(rng)), index=rng) - - ts_local = ts.tz_localize(tzstr) - - result = ts_local.at_time(time(10, 0)) - expected = ts.at_time(time(10, 0)).tz_localize(tzstr) - tm.assert_series_equal(result, expected) - assert timezones.tz_compare(result.index.tz, tz) - - t1, t2 = time(10, 0), time(11, 0) - result = ts_local.between_time(t1, t2) - expected = ts.between_time(t1, t2).tz_localize(tzstr) - tm.assert_series_equal(result, expected) - assert timezones.tz_compare(result.index.tz, tz) - @pytest.mark.parametrize("tzstr", ["Europe/Berlin", "dateutil/Europe/Berlin"]) def test_getitem_pydatetime_tz(self, tzstr): tz = timezones.maybe_get_tz(tzstr) @@ -335,14 +62,6 @@ def test_getitem_pydatetime_tz(self, tzstr): time_datetime = conversion.localize_pydatetime(dt, tz) assert ts[time_pandas] == ts[time_datetime] - def test_series_truncate_datetimeindex_tz(self): - # GH 9243 - idx = date_range("4/1/2005", "4/30/2005", freq="D", tz="US/Pacific") - s = Series(range(len(idx)), index=idx) - result = s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4)) - expected = Series([1, 2, 3], index=idx[1:4]) - tm.assert_series_equal(result, expected) - @pytest.mark.parametrize("copy", [True, False]) @pytest.mark.parametrize( "method, tz", [["tz_localize", None], ["tz_convert", "Europe/Berlin"]] @@ -357,10 +76,3 @@ def test_tz_localize_convert_copy_inplace_mutate(self, copy, method, tz): np.arange(0, 5), index=date_range("20131027", periods=5, freq="1H", tz=tz) ) tm.assert_series_equal(result, expected) - - def test_constructor_data_aware_dtype_naive(self, tz_aware_fixture): - # GH 25843 - tz = tz_aware_fixture - result = Series([Timestamp("2019", tz=tz)], dtype="datetime64[ns]") - expected = Series([Timestamp("2019")]) - tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_ufunc.py b/pandas/tests/series/test_ufunc.py index ece7f1f21ab23..536f15ea75d69 100644 --- a/pandas/tests/series/test_ufunc.py +++ b/pandas/tests/series/test_ufunc.py @@ -287,7 +287,7 @@ def __eq__(self, other) -> bool: return type(other) is Thing and self.value == other.value def __repr__(self) -> str: - return "Thing({})".format(self.value) + return f"Thing({self.value})" s = pd.Series([Thing(1), Thing(2)]) result = np.add(s, Thing(1)) diff --git a/pandas/tests/series/test_validate.py b/pandas/tests/series/test_validate.py index c4311f507f7ee..511d24ca7fa29 100644 --- a/pandas/tests/series/test_validate.py +++ b/pandas/tests/series/test_validate.py @@ -1,20 +1,18 @@ import pytest -class TestSeriesValidate: +@pytest.mark.parametrize( + "func", + ["reset_index", "_set_name", "sort_values", "sort_index", "rename", "dropna"], +) +@pytest.mark.parametrize("inplace", [1, "True", [1, 2, 3], 5.0]) +def test_validate_bool_args(string_series, func, inplace): """Tests for error handling related to data types of method arguments.""" + msg = 'For argument "inplace" expected type bool' + kwargs = dict(inplace=inplace) - @pytest.mark.parametrize( - "func", - ["reset_index", "_set_name", "sort_values", "sort_index", "rename", "dropna"], - ) - @pytest.mark.parametrize("inplace", [1, "True", [1, 2, 3], 5.0]) - def test_validate_bool_args(self, string_series, func, inplace): - msg = 'For argument "inplace" expected type bool' - kwargs = dict(inplace=inplace) + if func == "_set_name": + kwargs["name"] = "hello" - if func == "_set_name": - kwargs["name"] = "hello" - - with pytest.raises(ValueError, match=msg): - getattr(string_series, func)(**kwargs) + with pytest.raises(ValueError, match=msg): + getattr(string_series, func)(**kwargs) diff --git a/pandas/tests/test_compat.py b/pandas/tests/test_compat.py deleted file mode 100644 index 4ff8b0b31e85e..0000000000000 --- a/pandas/tests/test_compat.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Testing that functions from compat work as expected -""" diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index 02898988ca8aa..b2a85b539fd86 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -19,7 +19,7 @@ def import_module(name): try: return importlib.import_module(name) except ModuleNotFoundError: # noqa - pytest.skip("skipping as {} not available".format(name)) + pytest.skip(f"skipping as {name} not available") @pytest.fixture @@ -107,6 +107,7 @@ def test_pandas_datareader(): # importing from pandas, Cython import warning @pytest.mark.filterwarnings("ignore:can't resolve:ImportWarning") +@pytest.mark.skip(reason="Anaconda installation issue - GH32144") def test_geopandas(): geopandas = import_module("geopandas") # noqa diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index d72c00ceb0045..515d798fe4322 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -17,6 +17,7 @@ "EmptyDataError", "ParserWarning", "MergeError", + "OptionError", ], ) def test_exception_importable(exc): diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py index b377ca2869bd3..efaedfad1e093 100644 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -896,10 +896,10 @@ def test_stack_unstack_unordered_multiindex(self): values = np.arange(5) data = np.vstack( [ - ["b{}".format(x) for x in values], # b0, b1, .. - ["a{}".format(x) for x in values], + [f"b{x}" for x in values], # b0, b1, .. + [f"a{x}" for x in values], # a0, a1, .. ] - ) # a0, a1, .. + ) df = pd.DataFrame(data.T, columns=["b", "a"]) df.columns.name = "first" second_level_dict = {"x": df} diff --git a/pandas/tests/test_nanops.py b/pandas/tests/test_nanops.py index 2c5d028ebe42e..f7e652eb78e2d 100644 --- a/pandas/tests/test_nanops.py +++ b/pandas/tests/test_nanops.py @@ -750,8 +750,8 @@ def test_ndarray(self): # Test non-convertible string ndarray s_values = np.array(["foo", "bar", "baz"], dtype=object) - msg = r"could not convert string to float: '(foo|baz)'" - with pytest.raises(ValueError, match=msg): + msg = r"Could not convert .* to numeric" + with pytest.raises(TypeError, match=msg): nanops._ensure_numeric(s_values) def test_convertable_values(self): @@ -993,7 +993,6 @@ def prng(self): class TestDatetime64NaNOps: @pytest.mark.parametrize("tz", [None, "UTC"]) - @pytest.mark.xfail(reason="disabled") # Enabling mean changes the behavior of DataFrame.mean # See https://github.com/pandas-dev/pandas/issues/24752 def test_nanmean(self, tz): diff --git a/pandas/tests/test_optional_dependency.py b/pandas/tests/test_optional_dependency.py index ce527214e55e7..e5ed69b7703b1 100644 --- a/pandas/tests/test_optional_dependency.py +++ b/pandas/tests/test_optional_dependency.py @@ -22,12 +22,12 @@ def test_xlrd_version_fallback(): import_optional_dependency("xlrd") -def test_bad_version(): +def test_bad_version(monkeypatch): name = "fakemodule" module = types.ModuleType(name) module.__version__ = "0.9.0" sys.modules[name] = module - VERSIONS[name] = "1.0.0" + monkeypatch.setitem(VERSIONS, name, "1.0.0") match = "Pandas requires .*1.0.0.* of .fakemodule.*'0.9.0'" with pytest.raises(ImportError, match=match): @@ -42,11 +42,11 @@ def test_bad_version(): assert result is module -def test_no_version_raises(): +def test_no_version_raises(monkeypatch): name = "fakemodule" module = types.ModuleType(name) sys.modules[name] = module - VERSIONS[name] = "1.0.0" + monkeypatch.setitem(VERSIONS, name, "1.0.0") with pytest.raises(ImportError, match="Can't determine .* fakemodule"): import_optional_dependency(name) diff --git a/pandas/tests/test_register_accessor.py b/pandas/tests/test_register_accessor.py index 08a5581886522..d839936f731a3 100644 --- a/pandas/tests/test_register_accessor.py +++ b/pandas/tests/test_register_accessor.py @@ -9,7 +9,8 @@ @contextlib.contextmanager def ensure_removed(obj, attr): """Ensure that an attribute added to 'obj' during the test is - removed when we're done""" + removed when we're done + """ try: yield finally: diff --git a/pandas/tests/test_strings.py b/pandas/tests/test_strings.py index 62d26dacde67b..1338d801e39f4 100644 --- a/pandas/tests/test_strings.py +++ b/pandas/tests/test_strings.py @@ -7,6 +7,7 @@ from pandas._libs import lib +import pandas as pd from pandas import DataFrame, Index, MultiIndex, Series, concat, isna, notna import pandas._testing as tm import pandas.core.strings as strings @@ -207,6 +208,9 @@ def test_api_per_dtype(self, index_or_series, dtype, any_skipna_inferred_dtype): box = index_or_series inferred_dtype, values = any_skipna_inferred_dtype + if dtype == "category" and len(values) and values[1] is pd.NA: + pytest.xfail(reason="Categorical does not yet support pd.NA") + t = box(values, dtype=dtype) # explicit dtype to avoid casting # TODO: get rid of these xfails diff --git a/pandas/tests/tools/test_numeric.py b/pandas/tests/tools/test_numeric.py index 2fd39d5a7b703..19385e797467c 100644 --- a/pandas/tests/tools/test_numeric.py +++ b/pandas/tests/tools/test_numeric.py @@ -308,7 +308,7 @@ def test_really_large_in_arr_consistent(large_val, signed, multiple_elts, errors if errors in (None, "raise"): index = int(multiple_elts) - msg = "Integer out of range. at position {index}".format(index=index) + msg = f"Integer out of range. at position {index}" with pytest.raises(ValueError, match=msg): to_numeric(arr, **kwargs) diff --git a/pandas/tests/tools/test_to_time.py b/pandas/tests/tools/test_to_time.py new file mode 100644 index 0000000000000..17ab492aca725 --- /dev/null +++ b/pandas/tests/tools/test_to_time.py @@ -0,0 +1,58 @@ +from datetime import time + +import numpy as np +import pytest + +import pandas.util._test_decorators as td + +from pandas import Series +import pandas._testing as tm +from pandas.core.tools.datetimes import to_time + + +class TestToTime: + @td.skip_if_has_locale + def test_parsers_time(self): + # GH#11818 + strings = [ + "14:15", + "1415", + "2:15pm", + "0215pm", + "14:15:00", + "141500", + "2:15:00pm", + "021500pm", + time(14, 15), + ] + expected = time(14, 15) + + for time_string in strings: + assert to_time(time_string) == expected + + new_string = "14.15" + msg = r"Cannot convert arg \['14\.15'\] to a time" + with pytest.raises(ValueError, match=msg): + to_time(new_string) + assert to_time(new_string, format="%H.%M") == expected + + arg = ["14:15", "20:20"] + expected_arr = [time(14, 15), time(20, 20)] + assert to_time(arg) == expected_arr + assert to_time(arg, format="%H:%M") == expected_arr + assert to_time(arg, infer_time_format=True) == expected_arr + assert to_time(arg, format="%I:%M%p", errors="coerce") == [None, None] + + res = to_time(arg, format="%I:%M%p", errors="ignore") + tm.assert_numpy_array_equal(res, np.array(arg, dtype=np.object_)) + + with pytest.raises(ValueError): + to_time(arg, format="%I:%M%p", errors="raise") + + tm.assert_series_equal( + to_time(Series(arg, name="test")), Series(expected_arr, name="test") + ) + + res = to_time(np.array(arg)) + assert isinstance(res, list) + assert res == expected_arr diff --git a/pandas/tests/tseries/frequencies/test_inference.py b/pandas/tests/tseries/frequencies/test_inference.py index c4660417599a8..c32ad5087ab9e 100644 --- a/pandas/tests/tseries/frequencies/test_inference.py +++ b/pandas/tests/tseries/frequencies/test_inference.py @@ -178,7 +178,7 @@ def test_infer_freq_delta(base_delta_code_pair, count): inc = base_delta * count index = DatetimeIndex([b + inc * j for j in range(3)]) - exp_freq = "{count:d}{code}".format(count=count, code=code) if count > 1 else code + exp_freq = f"{count:d}{code}" if count > 1 else code assert frequencies.infer_freq(index) == exp_freq @@ -202,13 +202,11 @@ def test_infer_freq_custom(base_delta_code_pair, constructor): def test_weekly_infer(periods, day): - _check_generated_range("1/1/2000", periods, "W-{day}".format(day=day)) + _check_generated_range("1/1/2000", periods, f"W-{day}") def test_week_of_month_infer(periods, day, count): - _check_generated_range( - "1/1/2000", periods, "WOM-{count}{day}".format(count=count, day=day) - ) + _check_generated_range("1/1/2000", periods, f"WOM-{count}{day}") @pytest.mark.parametrize("freq", ["M", "BM", "BMS"]) @@ -217,14 +215,12 @@ def test_monthly_infer(periods, freq): def test_quarterly_infer(month, periods): - _check_generated_range("1/1/2000", periods, "Q-{month}".format(month=month)) + _check_generated_range("1/1/2000", periods, f"Q-{month}") @pytest.mark.parametrize("annual", ["A", "BA"]) def test_annually_infer(month, periods, annual): - _check_generated_range( - "1/1/2000", periods, "{annual}-{month}".format(annual=annual, month=month) - ) + _check_generated_range("1/1/2000", periods, f"{annual}-{month}") @pytest.mark.parametrize( diff --git a/pandas/tests/tseries/holiday/test_calendar.py b/pandas/tests/tseries/holiday/test_calendar.py index 5b4a7c74b1af1..cd3b1aab33a2a 100644 --- a/pandas/tests/tseries/holiday/test_calendar.py +++ b/pandas/tests/tseries/holiday/test_calendar.py @@ -98,3 +98,15 @@ class testCalendar(AbstractHolidayCalendar): Sat_before_Labor_Day_2031 = to_datetime("2031-08-30") next_working_day = Sat_before_Labor_Day_2031 + 0 * workDay assert next_working_day == to_datetime("2031-09-02") + + +def test_no_holidays_calendar(): + # Test for issue #31415 + + class NoHolidaysCalendar(AbstractHolidayCalendar): + pass + + cal = NoHolidaysCalendar() + holidays = cal.holidays(Timestamp("01-Jan-2020"), Timestamp("01-Jan-2021")) + empty_index = DatetimeIndex([]) # Type is DatetimeIndex since return_name=False + tm.assert_index_equal(holidays, empty_index) diff --git a/pandas/tests/tseries/offsets/common.py b/pandas/tests/tseries/offsets/common.py index 71953fd095882..25837c0b6aee2 100644 --- a/pandas/tests/tseries/offsets/common.py +++ b/pandas/tests/tseries/offsets/common.py @@ -11,11 +11,11 @@ def assert_offset_equal(offset, base, expected): assert actual == expected assert actual_swapped == expected assert actual_apply == expected - except AssertionError: + except AssertionError as err: raise AssertionError( f"\nExpected: {expected}\nActual: {actual}\nFor Offset: {offset})" f"\nAt Date: {base}" - ) + ) from err def assert_is_on_offset(offset, date, expected): diff --git a/pandas/tests/tslibs/test_parse_iso8601.py b/pandas/tests/tslibs/test_parse_iso8601.py index a58f227c20c7f..1c01e826d9794 100644 --- a/pandas/tests/tslibs/test_parse_iso8601.py +++ b/pandas/tests/tslibs/test_parse_iso8601.py @@ -51,7 +51,7 @@ def test_parsers_iso8601(date_str, exp): ], ) def test_parsers_iso8601_invalid(date_str): - msg = 'Error parsing datetime string "{s}"'.format(s=date_str) + msg = f'Error parsing datetime string "{date_str}"' with pytest.raises(ValueError, match=msg): tslib._test_parse_iso8601(date_str) diff --git a/pandas/tests/tslibs/test_parsing.py b/pandas/tests/tslibs/test_parsing.py index c452d5b12ce01..dc7421ea63464 100644 --- a/pandas/tests/tslibs/test_parsing.py +++ b/pandas/tests/tslibs/test_parsing.py @@ -2,6 +2,7 @@ Tests for Timestamp parsing, aimed at pandas/_libs/tslibs/parsing.pyx """ from datetime import datetime +import re from dateutil.parser import parse import numpy as np @@ -24,7 +25,8 @@ def test_parse_time_string(): def test_parse_time_string_invalid_type(): # Raise on invalid input, don't just return it - with pytest.raises(TypeError): + msg = "Argument 'arg' has incorrect type (expected str, got tuple)" + with pytest.raises(TypeError, match=re.escape(msg)): parse_time_string((4, 5)) @@ -42,9 +44,9 @@ def test_parse_time_quarter_with_dash(dashed, normal): @pytest.mark.parametrize("dashed", ["-2Q1992", "2-Q1992", "4-4Q1992"]) def test_parse_time_quarter_with_dash_error(dashed): - msg = "Unknown datetime string format, unable to parse: {dashed}" + msg = f"Unknown datetime string format, unable to parse: {dashed}" - with pytest.raises(parsing.DateParseError, match=msg.format(dashed=dashed)): + with pytest.raises(parsing.DateParseError, match=msg): parse_time_string(dashed) @@ -115,12 +117,12 @@ def test_parsers_quarter_invalid(date_str): if date_str == "6Q-20": msg = ( "Incorrect quarterly string is given, quarter " - "must be between 1 and 4: {date_str}" + f"must be between 1 and 4: {date_str}" ) else: - msg = "Unknown datetime string format, unable to parse: {date_str}" + msg = f"Unknown datetime string format, unable to parse: {date_str}" - with pytest.raises(ValueError, match=msg.format(date_str=date_str)): + with pytest.raises(ValueError, match=msg): parsing.parse_time_string(date_str) @@ -217,7 +219,8 @@ def test_try_parse_dates(): def test_parse_time_string_check_instance_type_raise_exception(): # issue 20684 - with pytest.raises(TypeError): + msg = "Argument 'arg' has incorrect type (expected str, got tuple)" + with pytest.raises(TypeError, match=re.escape(msg)): parse_time_string((1, 2, 3)) result = parse_time_string("2019") diff --git a/pandas/tests/util/test_assert_numpy_array_equal.py b/pandas/tests/util/test_assert_numpy_array_equal.py index c8ae9ebdd8651..d29ddedd2fdd6 100644 --- a/pandas/tests/util/test_assert_numpy_array_equal.py +++ b/pandas/tests/util/test_assert_numpy_array_equal.py @@ -1,6 +1,7 @@ import numpy as np import pytest +import pandas as pd from pandas import Timestamp import pandas._testing as tm @@ -175,3 +176,38 @@ def test_numpy_array_equal_copy_flag(other_type, check_same): tm.assert_numpy_array_equal(a, other, check_same=check_same) else: tm.assert_numpy_array_equal(a, other, check_same=check_same) + + +def test_numpy_array_equal_contains_na(): + # https://github.com/pandas-dev/pandas/issues/31881 + a = np.array([True, False]) + b = np.array([True, pd.NA], dtype=object) + + msg = """numpy array are different + +numpy array values are different \\(50.0 %\\) +\\[left\\]: \\[True, False\\] +\\[right\\]: \\[True, \\]""" + + with pytest.raises(AssertionError, match=msg): + tm.assert_numpy_array_equal(a, b) + + +def test_numpy_array_equal_identical_na(nulls_fixture): + a = np.array([nulls_fixture], dtype=object) + + tm.assert_numpy_array_equal(a, a) + + +def test_numpy_array_equal_different_na(): + a = np.array([np.nan], dtype=object) + b = np.array([pd.NA], dtype=object) + + msg = """numpy array are different + +numpy array values are different \\(100.0 %\\) +\\[left\\]: \\[nan\\] +\\[right\\]: \\[\\]""" + + with pytest.raises(AssertionError, match=msg): + tm.assert_numpy_array_equal(a, b) diff --git a/pandas/tests/util/test_doc.py b/pandas/tests/util/test_doc.py new file mode 100644 index 0000000000000..7e5e24456b9a7 --- /dev/null +++ b/pandas/tests/util/test_doc.py @@ -0,0 +1,88 @@ +from textwrap import dedent + +from pandas.util._decorators import doc + + +@doc(method="cumsum", operation="sum") +def cumsum(whatever): + """ + This is the {method} method. + + It computes the cumulative {operation}. + """ + + +@doc( + cumsum, + """ + Examples + -------- + + >>> cumavg([1, 2, 3]) + 2 + """, + method="cumavg", + operation="average", +) +def cumavg(whatever): + pass + + +@doc(cumsum, method="cummax", operation="maximum") +def cummax(whatever): + pass + + +@doc(cummax, method="cummin", operation="minimum") +def cummin(whatever): + pass + + +def test_docstring_formatting(): + docstr = dedent( + """ + This is the cumsum method. + + It computes the cumulative sum. + """ + ) + assert cumsum.__doc__ == docstr + + +def test_docstring_appending(): + docstr = dedent( + """ + This is the cumavg method. + + It computes the cumulative average. + + Examples + -------- + + >>> cumavg([1, 2, 3]) + 2 + """ + ) + assert cumavg.__doc__ == docstr + + +def test_doc_template_from_func(): + docstr = dedent( + """ + This is the cummax method. + + It computes the cumulative maximum. + """ + ) + assert cummax.__doc__ == docstr + + +def test_inherit_doc_template(): + docstr = dedent( + """ + This is the cummin method. + + It computes the cumulative minimum. + """ + ) + assert cummin.__doc__ == docstr diff --git a/pandas/tests/util/test_hashing.py b/pandas/tests/util/test_hashing.py index c856585f20138..6411b9ab654f1 100644 --- a/pandas/tests/util/test_hashing.py +++ b/pandas/tests/util/test_hashing.py @@ -178,23 +178,6 @@ def test_multiindex_objects(): assert mi.equals(recons) assert Index(mi.values).equals(Index(recons.values)) - # _hashed_values and hash_pandas_object(..., index=False) equivalency. - expected = hash_pandas_object(mi, index=False).values - result = mi._hashed_values - - tm.assert_numpy_array_equal(result, expected) - - expected = hash_pandas_object(recons, index=False).values - result = recons._hashed_values - - tm.assert_numpy_array_equal(result, expected) - - expected = mi._hashed_values - result = recons._hashed_values - - # Values should match, but in different order. - tm.assert_numpy_array_equal(np.sort(result), np.sort(expected)) - @pytest.mark.parametrize( "obj", diff --git a/pandas/tests/util/test_show_versions.py b/pandas/tests/util/test_show_versions.py new file mode 100644 index 0000000000000..e36ea662fac8b --- /dev/null +++ b/pandas/tests/util/test_show_versions.py @@ -0,0 +1,40 @@ +import re + +import pytest + +import pandas as pd + + +@pytest.mark.filterwarnings( + # openpyxl + "ignore:defusedxml.lxml is no longer supported:DeprecationWarning" +) +@pytest.mark.filterwarnings( + # html5lib + "ignore:Using or importing the ABCs from:DeprecationWarning" +) +@pytest.mark.filterwarnings( + # fastparquet + "ignore:pandas.core.index is deprecated:FutureWarning" +) +@pytest.mark.filterwarnings( + # pandas_datareader + "ignore:pandas.util.testing is deprecated:FutureWarning" +) +def test_show_versions(capsys): + # gh-32041 + pd.show_versions() + captured = capsys.readouterr() + result = captured.out + + # check header + assert "INSTALLED VERSIONS" in result + + # check full commit hash + assert re.search(r"commit\s*:\s[0-9a-f]{40}\n", result) + + # check required dependency + assert re.search(r"numpy\s*:\s([0-9\.\+a-f]|dev)+\n", result) + + # check optional dependency + assert re.search(r"pyarrow\s*:\s([0-9\.]+|None)\n", result) diff --git a/pandas/tests/window/moments/test_moments_rolling.py b/pandas/tests/window/moments/test_moments_rolling.py index 83e4ee25558b5..f3a14971ef2e7 100644 --- a/pandas/tests/window/moments/test_moments_rolling.py +++ b/pandas/tests/window/moments/test_moments_rolling.py @@ -860,7 +860,7 @@ def get_result(obj, window, min_periods=None, center=False): tm.assert_series_equal(result, expected) # shifter index - s = ["x{x:d}".format(x=x) for x in range(12)] + s = [f"x{x:d}" for x in range(12)] if has_min_periods: minp = 10 @@ -1335,7 +1335,6 @@ def test_rolling_kurt_eq_value_fperr(self): def test_rolling_max_gh6297(self): """Replicate result expected in GH #6297""" - indices = [datetime(1975, 1, i) for i in range(1, 6)] # So that we can have 2 datapoints on one of the days indices.append(datetime(1975, 1, 3, 6, 0)) @@ -1438,13 +1437,9 @@ def test_rolling_median_memory_error(self): def test_rolling_min_max_numeric_types(self): # GH12373 - types_test = [np.dtype("f{}".format(width)) for width in [4, 8]] + types_test = [np.dtype(f"f{width}") for width in [4, 8]] types_test.extend( - [ - np.dtype("{}{}".format(sign, width)) - for width in [1, 2, 4, 8] - for sign in "ui" - ] + [np.dtype(f"{sign}{width}") for width in [1, 2, 4, 8] for sign in "ui"] ) for data_type in types_test: # Just testing that these don't throw exceptions and that diff --git a/pandas/tests/window/test_grouper.py b/pandas/tests/window/test_grouper.py index 355ef3a90d424..5b2687271f9d6 100644 --- a/pandas/tests/window/test_grouper.py +++ b/pandas/tests/window/test_grouper.py @@ -190,3 +190,21 @@ def test_expanding_apply(self, raw): result = r.apply(lambda x: x.sum(), raw=raw) expected = g.apply(lambda x: x.expanding().apply(lambda y: y.sum(), raw=raw)) tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize("expected_value,raw_value", [[1.0, True], [0.0, False]]) + def test_groupby_rolling(self, expected_value, raw_value): + # GH 31754 + + def foo(x): + return int(isinstance(x, np.ndarray)) + + df = pd.DataFrame({"id": [1, 1, 1], "value": [1, 2, 3]}) + result = df.groupby("id").value.rolling(1).apply(foo, raw=raw_value) + expected = Series( + [expected_value] * 3, + index=pd.MultiIndex.from_tuples( + ((1, 0), (1, 1), (1, 2)), names=["id", None] + ), + name="value", + ) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/window/test_pairwise.py b/pandas/tests/window/test_pairwise.py index 717273cff64ea..bb305e93a3cf1 100644 --- a/pandas/tests/window/test_pairwise.py +++ b/pandas/tests/window/test_pairwise.py @@ -1,8 +1,9 @@ import warnings +import numpy as np import pytest -from pandas import DataFrame, Series +from pandas import DataFrame, Series, date_range import pandas._testing as tm from pandas.core.algorithms import safe_sort @@ -181,3 +182,10 @@ def test_pairwise_with_series(self, f): for i, result in enumerate(results): if i > 0: self.compare(result, results[0]) + + def test_corr_freq_memory_error(self): + # GH 31789 + s = Series(range(5), index=date_range("2020", periods=5)) + result = s.rolling("12H").corr(s) + expected = Series([np.nan] * 5, index=date_range("2020", periods=5)) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/window/test_window.py b/pandas/tests/window/test_window.py index cc29ab4f2cd62..c7c45f0e5e0de 100644 --- a/pandas/tests/window/test_window.py +++ b/pandas/tests/window/test_window.py @@ -30,13 +30,13 @@ def test_constructor(self, which): # not valid for w in [2.0, "foo", np.array([2])]: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="min_periods must be an integer"): c(win_type="boxcar", window=2, min_periods=w) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="center must be a boolean"): c(win_type="boxcar", window=2, min_periods=1, center=w) for wt in ["foobar", 1]: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Invalid win_type"): c(win_type=wt, window=2) @td.skip_if_no_scipy diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index af34180fb3170..1a1b7e8e1bd08 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -141,8 +141,8 @@ def to_offset(freq) -> Optional[DateOffset]: delta = offset else: delta = delta + offset - except ValueError: - raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) + except ValueError as err: + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) from err else: delta = None @@ -173,8 +173,8 @@ def to_offset(freq) -> Optional[DateOffset]: delta = offset else: delta = delta + offset - except (ValueError, TypeError): - raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) + except (ValueError, TypeError) as err: + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) from err if delta is None: raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) @@ -223,9 +223,9 @@ def _get_offset(name: str) -> DateOffset: # handles case where there's no suffix (and will TypeError if too # many '-') offset = klass._from_name(*split[1:]) - except (ValueError, TypeError, KeyError): + except (ValueError, TypeError, KeyError) as err: # bad prefix or suffix - raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(name)) + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(name)) from err # cache _offset_map[name] = offset diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py index 62d7c26b590cc..fe30130e87c01 100644 --- a/pandas/tseries/holiday.py +++ b/pandas/tseries/holiday.py @@ -7,7 +7,7 @@ from pandas.errors import PerformanceWarning -from pandas import DateOffset, Series, Timestamp, date_range +from pandas import DateOffset, DatetimeIndex, Series, Timestamp, concat, date_range from pandas.tseries.offsets import Day, Easter @@ -406,17 +406,14 @@ def holidays(self, start=None, end=None, return_name=False): start = Timestamp(start) end = Timestamp(end) - holidays = None # If we don't have a cache or the dates are outside the prior cache, we # get them again if self._cache is None or start < self._cache[0] or end > self._cache[1]: - for rule in self.rules: - rule_holidays = rule.dates(start, end, return_name=True) - - if holidays is None: - holidays = rule_holidays - else: - holidays = holidays.append(rule_holidays) + holidays = [rule.dates(start, end, return_name=True) for rule in self.rules] + if holidays: + holidays = concat(holidays) + else: + holidays = Series(index=DatetimeIndex([]), dtype=object) self._cache = (start, end, holidays.sort_index()) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index e05cce9c49f4b..b6bbe008812cb 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -308,7 +308,6 @@ def apply_index(self, i): ------- y : DatetimeIndex """ - if type(self) is not DateOffset: raise NotImplementedError( f"DateOffset subclass {type(self).__name__} " @@ -1018,8 +1017,7 @@ def __init__( class CustomBusinessDay(_CustomMixin, BusinessDay): """ - DateOffset subclass representing possibly n custom business days, - excluding holidays. + DateOffset subclass representing custom business days excluding holidays. Parameters ---------- @@ -2532,12 +2530,12 @@ def _tick_comp(op): def f(self, other): try: return op(self.delta, other.delta) - except AttributeError: + except AttributeError as err: # comparing with a non-Tick object raise TypeError( f"Invalid comparison between {type(self).__name__} " f"and {type(other).__name__}" - ) + ) from err f.__name__ = f"__{op.__name__}__" return f @@ -2572,10 +2570,10 @@ def __add__(self, other): return self.apply(other) except ApplyTypeError: return NotImplemented - except OverflowError: + except OverflowError as err: raise OverflowError( f"the add operation between {self} and {other} will overflow" - ) + ) from err def __eq__(self, other: Any) -> bool: if isinstance(other, str): diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 0aab5a9c4113d..d854be062fcbb 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -55,7 +55,6 @@ def deprecate( The message to display in the warning. Default is '{name} is deprecated. Use {alt_name} instead.' """ - alt_name = alt_name or alternative.__name__ klass = klass or FutureWarning warning_msg = msg or f"{name} is deprecated, use {alt_name} instead" @@ -163,7 +162,6 @@ def deprecate_kwarg( future version please takes steps to stop use of 'cols' should raise warning """ - if mapping is not None and not hasattr(mapping, "get") and not callable(mapping): raise TypeError( "mapping from old to new argument values must be dict or callable!" @@ -247,6 +245,46 @@ def wrapper(*args, **kwargs) -> Callable[..., Any]: return decorate +def doc(*args: Union[str, Callable], **kwargs: str) -> Callable[[F], F]: + """ + A decorator take docstring templates, concatenate them and perform string + substitution on it. + + This decorator is robust even if func.__doc__ is None. This decorator will + add a variable "_docstr_template" to the wrapped function to save original + docstring template for potential usage. + + Parameters + ---------- + *args : str or callable + The string / docstring / docstring template to be appended in order + after default docstring under function. + **kwags : str + The string which would be used to format docstring template. + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args, **kwargs) -> Callable: + return func(*args, **kwargs) + + templates = [func.__doc__ if func.__doc__ else ""] + for arg in args: + if isinstance(arg, str): + templates.append(arg) + elif hasattr(arg, "_docstr_template"): + templates.append(arg._docstr_template) # type: ignore + elif arg.__doc__: + templates.append(arg.__doc__) + + wrapper._docstr_template = "".join(dedent(t) for t in templates) # type: ignore + wrapper.__doc__ = wrapper._docstr_template.format(**kwargs) # type: ignore + + return cast(F, wrapper) + + return decorator + + # Substitution and Appender are derived from matplotlib.docstring (1.1.0) # module https://matplotlib.org/users/license.html diff --git a/pandas/util/_print_versions.py b/pandas/util/_print_versions.py index fdfa436ce6536..f9502cc22b0c6 100644 --- a/pandas/util/_print_versions.py +++ b/pandas/util/_print_versions.py @@ -4,13 +4,23 @@ import os import platform import struct -import subprocess import sys from typing import List, Optional, Tuple, Union from pandas.compat._optional import VERSIONS, _get_version, import_optional_dependency +def _get_commit_hash() -> Optional[str]: + """ + Use vendored versioneer code to get git hash, which handles + git worktree correctly. + """ + from pandas._version import get_versions + + versions = get_versions() + return versions["full-revisionid"] + + def get_sys_info() -> List[Tuple[str, Optional[Union[str, int]]]]: """ Returns system information as a list @@ -18,20 +28,7 @@ def get_sys_info() -> List[Tuple[str, Optional[Union[str, int]]]]: blob: List[Tuple[str, Optional[Union[str, int]]]] = [] # get full commit hash - commit = None - if os.path.isdir(".git") and os.path.isdir("pandas"): - try: - pipe = subprocess.Popen( - 'git log --format="%H" -n 1'.split(" "), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - so, serr = pipe.communicate() - except (OSError, ValueError): - pass - else: - if pipe.returncode == 0: - commit = so.decode("utf-8").strip().strip('"') + commit = _get_commit_hash() blob.append(("commit", commit)) @@ -118,10 +115,10 @@ def show_versions(as_json=False): print("\nINSTALLED VERSIONS") print("------------------") for k, stat in sys_info: - print(f"{{k:<{maxlen}}}: {{stat}}") + print(f"{k:<{maxlen}}: {stat}") print("") for k, stat in deps_blob: - print(f"{{k:<{maxlen}}}: {{stat}}") + print(f"{k:<{maxlen}}: {stat}") def main() -> int: diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py index cd7fdd55a4d2c..25394dc6775d8 100644 --- a/pandas/util/_test_decorators.py +++ b/pandas/util/_test_decorators.py @@ -40,15 +40,15 @@ def test_foo(): def safe_import(mod_name: str, min_version: Optional[str] = None): """ - Parameters: - ----------- + Parameters + ---------- mod_name : str Name of the module to be imported min_version : str, default None Minimum required version of the specified mod_name - Returns: - -------- + Returns + ------- object The imported module if successful, or False """ diff --git a/pandas/util/_tester.py b/pandas/util/_tester.py index b299f3790ab22..1bdf0d8483c76 100644 --- a/pandas/util/_tester.py +++ b/pandas/util/_tester.py @@ -10,12 +10,12 @@ def test(extra_args=None): try: import pytest - except ImportError: - raise ImportError("Need pytest>=5.0.1 to run tests") + except ImportError as err: + raise ImportError("Need pytest>=5.0.1 to run tests") from err try: import hypothesis # noqa - except ImportError: - raise ImportError("Need hypothesis>=3.58 to run tests") + except ImportError as err: + raise ImportError("Need hypothesis>=3.58 to run tests") from err cmd = ["--skip-slow", "--skip-network", "--skip-db"] if extra_args: if not isinstance(extra_args, list): diff --git a/pandas/util/_validators.py b/pandas/util/_validators.py index a715094e65e98..682575cc9ed48 100644 --- a/pandas/util/_validators.py +++ b/pandas/util/_validators.py @@ -91,6 +91,7 @@ def validate_args(fname, args, max_fname_arg_count, compat_args): arguments **positionally** internally when calling downstream implementations, a dict ensures that the original order of the keyword arguments is enforced. + Raises ------ TypeError @@ -215,7 +216,8 @@ def validate_bool_kwarg(value, arg_name): def validate_axis_style_args(data, args, kwargs, arg_name, method_name): - """Argument handler for mixed index, columns / axis functions + """ + Argument handler for mixed index, columns / axis functions In an attempt to handle both `.method(index, columns)`, and `.method(arg, axis=.)`, we have to do some bad things to argument @@ -309,7 +311,8 @@ def validate_axis_style_args(data, args, kwargs, arg_name, method_name): def validate_fillna_kwargs(value, method, validate_scalar_dict_value=True): - """Validate the keyword arguments to 'fillna'. + """ + Validate the keyword arguments to 'fillna'. This checks that exactly one of 'value' and 'method' is specified. If 'method' is specified, this validates that it's a valid method. diff --git a/requirements-dev.txt b/requirements-dev.txt index 08cbef2c7fc6b..a469cbdd93ceb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,6 +15,7 @@ isort mypy==0.730 pycodestyle gitpython +gitdb2==2.0.6 sphinx nbconvert>=5.4.1 nbsphinx diff --git a/scripts/find_commits_touching_func.py b/scripts/find_commits_touching_func.py deleted file mode 100755 index 85675cb6df42b..0000000000000 --- a/scripts/find_commits_touching_func.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3 -# copyright 2013, y-p @ github -""" -Search the git history for all commits touching a named method - -You need the sh module to run this -WARNING: this script uses git clean -f, running it on a repo with untracked -files will probably erase them. - -Usage:: - $ ./find_commits_touching_func.py (see arguments below) -""" -import argparse -from collections import namedtuple -import logging -import os -import re - -from dateutil.parser import parse - -try: - import sh -except ImportError: - raise ImportError("The 'sh' package is required to run this script.") - - -desc = """ -Find all commits touching a specified function across the codebase. -""".strip() -argparser = argparse.ArgumentParser(description=desc) -argparser.add_argument( - "funcname", - metavar="FUNCNAME", - help="Name of function/method to search for changes on", -) -argparser.add_argument( - "-f", - "--file-masks", - metavar="f_re(,f_re)*", - default=[r"\.py.?$"], - help="comma separated list of regexes to match " - "filenames against\ndefaults all .py? files", -) -argparser.add_argument( - "-d", - "--dir-masks", - metavar="d_re(,d_re)*", - default=[], - help="comma separated list of regexes to match base path against", -) -argparser.add_argument( - "-p", - "--path-masks", - metavar="p_re(,p_re)*", - default=[], - help="comma separated list of regexes to match full file path against", -) -argparser.add_argument( - "-y", - "--saw-the-warning", - action="store_true", - default=False, - help="must specify this to run, acknowledge you " - "realize this will erase untracked files", -) -argparser.add_argument( - "--debug-level", - default="CRITICAL", - help="debug level of messages (DEBUG, INFO, etc...)", -) -args = argparser.parse_args() - - -lfmt = logging.Formatter(fmt="%(levelname)-8s %(message)s", datefmt="%m-%d %H:%M:%S") -shh = logging.StreamHandler() -shh.setFormatter(lfmt) -logger = logging.getLogger("findit") -logger.addHandler(shh) - -Hit = namedtuple("Hit", "commit path") -HASH_LEN = 8 - - -def clean_checkout(comm): - h, s, d = get_commit_vitals(comm) - if len(s) > 60: - s = s[:60] + "..." - s = s.split("\n")[0] - logger.info("CO: %s %s" % (comm, s)) - - sh.git("checkout", comm, _tty_out=False) - sh.git("clean", "-f") - - -def get_hits(defname, files=()): - cs = set() - for f in files: - try: - r = sh.git( - "blame", - "-L", - r"/def\s*{start}/,/def/".format(start=defname), - f, - _tty_out=False, - ) - except sh.ErrorReturnCode_128: - logger.debug("no matches in %s" % f) - continue - - lines = r.strip().splitlines()[:-1] - # remove comment lines - lines = [x for x in lines if not re.search(r"^\w+\s*\(.+\)\s*#", x)] - hits = set(map(lambda x: x.split(" ")[0], lines)) - cs.update({Hit(commit=c, path=f) for c in hits}) - - return cs - - -def get_commit_info(c, fmt, sep="\t"): - r = sh.git( - "log", - "--format={}".format(fmt), - "{}^..{}".format(c, c), - "-n", - "1", - _tty_out=False, - ) - return str(r).split(sep) - - -def get_commit_vitals(c, hlen=HASH_LEN): - h, s, d = get_commit_info(c, "%H\t%s\t%ci", "\t") - return h[:hlen], s, parse(d) - - -def file_filter(state, dirname, fnames): - if args.dir_masks and not any(re.search(x, dirname) for x in args.dir_masks): - return - for f in fnames: - p = os.path.abspath(os.path.join(os.path.realpath(dirname), f)) - if any(re.search(x, f) for x in args.file_masks) or any( - re.search(x, p) for x in args.path_masks - ): - if os.path.isfile(p): - state["files"].append(p) - - -def search(defname, head_commit="HEAD"): - HEAD, s = get_commit_vitals("HEAD")[:2] - logger.info("HEAD at %s: %s" % (HEAD, s)) - done_commits = set() - # allhits = set() - files = [] - state = dict(files=files) - os.walk(".", file_filter, state) - # files now holds a list of paths to files - - # seed with hits from q - allhits = set(get_hits(defname, files=files)) - q = {HEAD} - try: - while q: - h = q.pop() - clean_checkout(h) - hits = get_hits(defname, files=files) - for x in hits: - prevc = get_commit_vitals(x.commit + "^")[0] - if prevc not in done_commits: - q.add(prevc) - allhits.update(hits) - done_commits.add(h) - - logger.debug("Remaining: %s" % q) - finally: - logger.info("Restoring HEAD to %s" % HEAD) - clean_checkout(HEAD) - return allhits - - -def pprint_hits(hits): - SUBJ_LEN = 50 - PATH_LEN = 20 - hits = list(hits) - max_p = 0 - for hit in hits: - p = hit.path.split(os.path.realpath(os.curdir) + os.path.sep)[-1] - max_p = max(max_p, len(p)) - - if max_p < PATH_LEN: - SUBJ_LEN += PATH_LEN - max_p - PATH_LEN = max_p - - def sorter(i): - h, s, d = get_commit_vitals(hits[i].commit) - return hits[i].path, d - - print( - ("\nThese commits touched the %s method in these files on these dates:\n") - % args.funcname - ) - for i in sorted(range(len(hits)), key=sorter): - hit = hits[i] - h, s, d = get_commit_vitals(hit.commit) - p = hit.path.split(os.path.realpath(os.curdir) + os.path.sep)[-1] - - fmt = "{:%d} {:10} {:<%d} {:<%d}" % (HASH_LEN, SUBJ_LEN, PATH_LEN) - if len(s) > SUBJ_LEN: - s = s[: SUBJ_LEN - 5] + " ..." - print(fmt.format(h[:HASH_LEN], d.isoformat()[:10], s, p[-20:])) - - print("\n") - - -def main(): - if not args.saw_the_warning: - argparser.print_help() - print( - """ -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -WARNING: -this script uses git clean -f, running it on a repo with untracked files. -It's recommended that you make a fresh clone and run from its root directory. -You must specify the -y argument to ignore this warning. -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -""" - ) - return - if isinstance(args.file_masks, str): - args.file_masks = args.file_masks.split(",") - if isinstance(args.path_masks, str): - args.path_masks = args.path_masks.split(",") - if isinstance(args.dir_masks, str): - args.dir_masks = args.dir_masks.split(",") - - logger.setLevel(getattr(logging, args.debug_level)) - - hits = search(args.funcname) - pprint_hits(hits) - - -if __name__ == "__main__": - import sys - - sys.exit(main()) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index d43086756769a..051bd5b9761ae 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -243,14 +243,12 @@ def pandas_validate(func_name: str): "EX03", error_code=err.error_code, error_message=err.message, - times_happening=" ({} times)".format(err.count) - if err.count > 1 - else "", + times_happening=f" ({err.count} times)" if err.count > 1 else "", ) ) examples_source_code = "".join(doc.examples_source_code) for wrong_import in ("numpy", "pandas"): - if "import {}".format(wrong_import) in examples_source_code: + if f"import {wrong_import}" in examples_source_code: result["errors"].append( pandas_error("EX04", imported_library=wrong_import) ) @@ -345,9 +343,7 @@ def header(title, width=80, char="#"): full_line = char * width side_len = (width - len(title) - 2) // 2 adj = "" if len(title) % 2 == 0 else " " - title_line = "{side} {title}{adj} {side}".format( - side=char * side_len, title=title, adj=adj - ) + title_line = f"{char * side_len} {title}{adj} {char * side_len}" return f"\n{full_line}\n{title_line}\n{full_line}\n\n" diff --git a/setup.cfg b/setup.cfg index c298aa652824c..61d5b1030a500 100644 --- a/setup.cfg +++ b/setup.cfg @@ -135,12 +135,6 @@ ignore_errors=True [mypy-pandas.tests.arithmetic.test_datetime64] ignore_errors=True -[mypy-pandas.tests.extension.decimal.test_decimal] -ignore_errors=True - -[mypy-pandas.tests.extension.json.test_json] -ignore_errors=True - [mypy-pandas.tests.indexes.datetimes.test_tools] ignore_errors=True @@ -153,15 +147,9 @@ check_untyped_defs=False [mypy-pandas._version] check_untyped_defs=False -[mypy-pandas.core.arrays.categorical] -check_untyped_defs=False - [mypy-pandas.core.arrays.interval] check_untyped_defs=False -[mypy-pandas.core.arrays.sparse.array] -check_untyped_defs=False - [mypy-pandas.core.base] check_untyped_defs=False @@ -240,9 +228,6 @@ check_untyped_defs=False [mypy-pandas.core.strings] check_untyped_defs=False -[mypy-pandas.core.tools.datetimes] -check_untyped_defs=False - [mypy-pandas.core.window.common] check_untyped_defs=False diff --git a/web/pandas/community/coc.md b/web/pandas/community/coc.md index bf62f4e00f847..d2af9c3fdd25b 100644 --- a/web/pandas/community/coc.md +++ b/web/pandas/community/coc.md @@ -20,6 +20,9 @@ Examples of unacceptable behavior by participants include: addresses, without explicit permission * Other unethical or unprofessional conduct +Furthermore, we encourage inclusive behavior - for example, +please don’t say “hey guys!” but “hey everyone!”. + Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or diff --git a/web/pandas/config.yml b/web/pandas/config.yml index 83eb152c9d944..a52c580f23530 100644 --- a/web/pandas/config.yml +++ b/web/pandas/config.yml @@ -54,7 +54,7 @@ blog: - https://dev.pandas.io/pandas-blog/feeds/all.atom.xml - https://wesmckinney.com/feeds/pandas.atom.xml - https://tomaugspurger.github.io/feed - - https://jorisvandenbossche.github.io/feeds/all.atom.xml + - https://jorisvandenbossche.github.io/feeds/pandas.atom.xml - https://datapythonista.github.io/blog/feeds/pandas.atom.xml - https://numfocus.org/tag/pandas/feed/ maintainers: diff --git a/web/pandas_web.py b/web/pandas_web.py index a34a31feabce0..38ab78f5690e7 100755 --- a/web/pandas_web.py +++ b/web/pandas_web.py @@ -34,12 +34,13 @@ import time import typing -import feedparser import jinja2 -import markdown import requests import yaml +import feedparser +import markdown + class Preprocessors: """