Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/_util.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import contextlib
4import datetime as dt
5from typing import (
6 TYPE_CHECKING,
7 Any,
8 Literal,
9 cast,
10)
11import warnings
12import zoneinfo
14import numpy as np
16from pandas._config import using_string_dtype
18from pandas._libs import lib
19from pandas._libs.tslibs import timezones
20from pandas.compat import (
21 pa_version_under18p0,
22 pa_version_under19p0,
23 pa_version_under25p0,
24)
25from pandas.compat._optional import import_optional_dependency
26from pandas.errors import Pandas4Warning
28from pandas.core.dtypes.common import pandas_dtype
30import pandas as pd
32if TYPE_CHECKING:
33 from collections.abc import (
34 Callable,
35 Generator,
36 Hashable,
37 Sequence,
38 )
40 import pyarrow
42 from pandas._typing import (
43 DtypeArg,
44 DtypeBackend,
45 )
47 from pandas.core.dtypes.base import ExtensionDtype
50pytz = import_optional_dependency("pytz", errors="ignore")
53@contextlib.contextmanager
54def suppress_pyarrow_values_warning() -> Generator[None]:
55 """
56 Suppress the deprecation warning pyarrow triggers by calling ``.values``
57 on timezone-aware data when converting pandas objects.
59 Remove once the minimum pyarrow (26.0) no longer does this, see GH#68426 and
60 apache/arrow#51302.
61 """
62 with warnings.catch_warnings():
63 warnings.filterwarnings(
64 "ignore",
65 r"(Series|DatetimeIndex)\.values returning an ndarray that drops "
66 "timezone information",
67 Pandas4Warning,
68 )
69 yield
72def _arrow_dtype_mapping() -> dict[pyarrow.DataType, Any]:
73 pa = import_optional_dependency("pyarrow")
74 return {
75 pa.int8(): pd.Int8Dtype(),
76 pa.int16(): pd.Int16Dtype(),
77 pa.int32(): pd.Int32Dtype(),
78 pa.int64(): pd.Int64Dtype(),
79 pa.uint8(): pd.UInt8Dtype(),
80 pa.uint16(): pd.UInt16Dtype(),
81 pa.uint32(): pd.UInt32Dtype(),
82 pa.uint64(): pd.UInt64Dtype(),
83 pa.bool_(): pd.BooleanDtype(),
84 pa.string(): pd.StringDtype(),
85 pa.float32(): pd.Float32Dtype(),
86 pa.float64(): pd.Float64Dtype(),
87 pa.string(): pd.StringDtype(),
88 pa.large_string(): pd.StringDtype(),
89 }
92def _arrow_string_types_mapper() -> Callable[[pyarrow.DataType], ExtensionDtype | None]:
93 pa = import_optional_dependency("pyarrow")
95 mapping = {
96 pa.string(): pd.StringDtype(na_value=np.nan),
97 pa.large_string(): pd.StringDtype(na_value=np.nan),
98 }
99 if not pa_version_under18p0:
100 mapping[pa.string_view()] = pd.StringDtype(na_value=np.nan)
102 return mapping.get
105def arrow_table_to_pandas(
106 table: pyarrow.Table,
107 dtype_backend: DtypeBackend | Literal["numpy"] | lib.NoDefault = lib.no_default,
108 null_to_int64: bool = False,
109 to_pandas_kwargs: dict[str, Any] | None = None,
110 dtype: DtypeArg | None = None,
111 names: Sequence[Hashable] | None = None,
112) -> pd.DataFrame:
113 pa = import_optional_dependency("pyarrow")
115 to_pandas_kwargs = {} if to_pandas_kwargs is None else to_pandas_kwargs
117 types_mapper: (
118 type[pd.ArrowDtype] | Callable[[pyarrow.DataType], ExtensionDtype | None] | None
119 )
120 if dtype_backend == "numpy_nullable":
121 mapping = _arrow_dtype_mapping()
122 if null_to_int64:
123 # Modify the default mapping to also map null to Int64
124 # (to match other engines - only for CSV parser)
125 mapping[pa.null()] = pd.Int64Dtype()
126 types_mapper = mapping.get
127 elif dtype_backend == "pyarrow":
128 types_mapper = pd.ArrowDtype
129 elif using_string_dtype():
130 if pa_version_under19p0:
131 types_mapper = _arrow_string_types_mapper()
132 elif dtype is not None:
133 # GH#56136 Avoid lossy conversion to float64
134 # We'll convert to numpy below if
135 types_mapper = {
136 pa.int8(): pd.Int8Dtype(),
137 pa.int16(): pd.Int16Dtype(),
138 pa.int32(): pd.Int32Dtype(),
139 pa.int64(): pd.Int64Dtype(),
140 }.get
141 else:
142 types_mapper = None
143 elif dtype_backend is lib.no_default or dtype_backend == "numpy":
144 if dtype is not None:
145 # GH#56136 Avoid lossy conversion to float64
146 # We'll convert to numpy below if
147 types_mapper = {
148 pa.int8(): pd.Int8Dtype(),
149 pa.int16(): pd.Int16Dtype(),
150 pa.int32(): pd.Int32Dtype(),
151 pa.int64(): pd.Int64Dtype(),
152 }.get
153 else:
154 types_mapper = None
155 else:
156 raise NotImplementedError
158 df = table.to_pandas(types_mapper=types_mapper, **to_pandas_kwargs)
159 df = _post_convert_dtypes(df, dtype_backend, dtype, names)
160 df = _normalize_timezone_dtypes(df)
161 return df
164def _post_convert_dtypes(
165 df: pd.DataFrame,
166 dtype_backend: DtypeBackend | Literal["numpy"] | lib.NoDefault,
167 dtype: DtypeArg | None,
168 names: Sequence[Hashable] | None,
169) -> pd.DataFrame:
170 if dtype is not None and (
171 dtype_backend is lib.no_default or dtype_backend == "numpy"
172 ):
173 # GH#56136 apply any user-provided dtype, and convert any IntegerDtype
174 # columns the user didn't explicitly ask for.
175 if isinstance(dtype, dict):
176 if names is not None:
177 df.columns = names
179 cmp_dtypes = {
180 pd.Int8Dtype(),
181 pd.Int16Dtype(),
182 pd.Int32Dtype(),
183 pd.Int64Dtype(),
184 }
185 for col in df.columns:
186 if col not in dtype and df[col].dtype in cmp_dtypes:
187 # Any key that the user didn't explicitly specify
188 # that got converted to IntegerDtype now gets converted
189 # to numpy dtype.
190 dtype[col] = df[col].dtype.numpy_dtype
192 # Ignore non-existent columns from dtype mapping
193 # like other parsers do
194 dtype = {
195 key: pandas_dtype(dtype[key]) for key in dtype if key in df.columns
196 }
198 # GH#65056 pyarrow returns datetime.date objects which trigger
199 # a deprecation warning when compared against DatetimeIndex
200 # categories. Convert to datetime64 first since the user can't
201 # control pyarrow's behavior.
202 for col, col_dtype in dtype.items():
203 if (
204 isinstance(col_dtype, pd.CategoricalDtype)
205 and col_dtype.categories is not None
206 and isinstance(col_dtype.categories, pd.DatetimeIndex)
207 and df[col].dtype == np.dtype("object")
208 and lib.infer_dtype(df[col]) == "date"
209 ):
210 df[col] = pd.to_datetime(df[col])
212 else:
213 dtype = pandas_dtype(dtype)
215 try:
216 df = df.astype(dtype)
217 except TypeError as err:
218 # GH#44901 reraise to keep api consistent
219 raise ValueError(str(err)) from err
221 # GH#56136 IntegerDtype was used above to avoid lossy float64
222 # conversion in pyarrow; convert back to numpy now that the data
223 # is categorical
224 # runtime import to avoid circular import; core.dtypes.cast imports
225 # this module at module scope
226 from pandas.core.arrays.integer import IntegerDtype
228 col_dtypes = df.dtypes
229 for i in range(len(df.columns)):
230 col_dtype = col_dtypes.iloc[i]
231 if not isinstance(col_dtype, pd.CategoricalDtype):
232 continue
233 cat_arr_dtype = col_dtype.categories.dtype
234 if not isinstance(cat_arr_dtype, IntegerDtype):
235 continue
236 if isinstance(dtype, dict):
237 requested = dtype.get(df.columns[i])
238 else:
239 requested = dtype
240 if (
241 isinstance(requested, pd.CategoricalDtype)
242 and requested.categories is not None
243 ):
244 # the user explicitly asked for these categories
245 continue
246 new_cat_dtype = pd.CategoricalDtype(
247 categories=col_dtype.categories.astype(cat_arr_dtype.numpy_dtype),
248 ordered=col_dtype.ordered,
249 )
250 df.isetitem(i, df.iloc[:, i].astype(new_cat_dtype))
252 if (
253 not using_string_dtype()
254 and dtype != "str"
255 and (dtype_backend is lib.no_default or dtype_backend == "numpy")
256 ):
257 # Convert any StringDtype columns back to object dtype (pyarrow always
258 # uses string dtype even when the infer_string option is False)
259 for i in range(len(df.columns)):
260 new_col = _maybe_convert_string_to_object(df.iloc[:, i])
261 if new_col is not None:
262 df.isetitem(i, new_col)
264 new_idx = _maybe_convert_string_index_to_object(df.index)
265 if new_idx is not None:
266 df.index = new_idx
267 new_cols = _maybe_convert_string_index_to_object(df.columns)
268 if new_cols is not None:
269 df.columns = new_cols
271 return df
274def _maybe_convert_string_to_object(
275 data: pd.Series | pd.Index,
276) -> pd.Series | pd.Index | None:
277 if isinstance(data.dtype, pd.StringDtype) and data.dtype.na_value is np.nan:
278 return data.astype("object").fillna(None)
279 elif isinstance(data.dtype, pd.CategoricalDtype):
280 cat_dtype = data.dtype.categories.dtype
281 if isinstance(cat_dtype, pd.StringDtype) and cat_dtype.na_value is np.nan:
282 # not astype: for ordered categoricals CategoricalDtype.__eq__
283 # ignores the categories' dtype, so astype would no-op
284 cat_arr = cast("pd.Categorical", data._values)
285 new_arr = cat_arr.set_categories(data.dtype.categories.astype("object"))
286 if isinstance(data, pd.Index):
287 return pd.CategoricalIndex(new_arr, name=data.name)
288 return pd.Series(new_arr, index=data.index, name=data.name, copy=False)
290 # no conversion needed
291 return None
294def _maybe_convert_string_index_to_object(index: pd.Index) -> pd.Index | None:
295 if isinstance(index, pd.MultiIndex):
296 if any(
297 isinstance(level.dtype, pd.StringDtype) and level.dtype.na_value is np.nan
298 for level in index.levels
299 ):
300 new_levels = []
301 for level in index.levels:
302 new_level = _maybe_convert_string_to_object(level)
303 if new_level is not None:
304 new_levels.append(new_level)
305 else:
306 new_levels.append(level)
307 return index.set_levels(new_levels)
308 return None
310 else:
311 return cast("pd.Index | None", _maybe_convert_string_to_object(index))
314def _normalize_pytz_timezone(tz: dt.tzinfo) -> dt.tzinfo:
315 """
316 If the input tz is a pytz timezone, attempt to convert it to "default"
317 tzinfo object (zoneinfo or datetime.timezone).
318 """
319 if not type(tz).__module__.startswith("pytz"):
320 # isinstance(col.dtype.tz, pytz.BaseTzInfo) does not included
321 # fixed offsets
322 return tz
324 if timezones.is_utc(tz):
325 return dt.UTC
327 if tz.zone is not None: # type: ignore[attr-defined]
328 try:
329 return zoneinfo.ZoneInfo(tz.zone) # type: ignore[attr-defined]
330 except Exception:
331 # some pytz timezones might not be available for zoneinfo
332 pass
334 if timezones.is_fixed_offset(tz):
335 # Convert pytz fixed offset to datetime.timezone
336 try:
337 offset = tz.utcoffset(None)
338 if offset is not None:
339 return dt.timezone(offset)
340 except Exception:
341 pass
343 return tz
346def _normalize_timezone_index(index: pd.Index) -> pd.Index:
347 if isinstance(index, pd.MultiIndex):
348 if any(isinstance(level.dtype, pd.DatetimeTZDtype) for level in index.levels):
349 levels = [_normalize_timezone_index(level) for level in index.levels]
350 return index.set_levels(levels)
352 return index
354 if isinstance(index.dtype, pd.DatetimeTZDtype):
355 normalized_tz = _normalize_pytz_timezone(index.dtype.tz)
356 if normalized_tz is not index.dtype.tz:
357 return index.tz_convert(normalized_tz) # type: ignore[attr-defined]
359 return index
362def _normalize_timezone_dtypes(df: pd.DataFrame) -> pd.DataFrame:
363 """
364 PyArrow below version 25 uses pytz by default for timezones, but pandas uses
365 zoneinfo / datetime.timezone since pandas 3.0.
367 Can be dropped once the minimum supported pyarrow is 25, which uses zoneinfo
368 itself (https://github.com/apache/arrow/pull/49694).
369 """
370 if pytz is not None and pa_version_under25p0:
371 # Convert any pytz timezones to zoneinfo / fixed offset timezones
372 if any(
373 isinstance(dtype, pd.DatetimeTZDtype)
374 for dtype in df._mgr.get_unique_dtypes()
375 ):
376 col_indices = df._select_dtypes_indices(pd.DatetimeTZDtype)
377 for i in col_indices:
378 col = df.iloc[:, i]
379 normalized_tz = _normalize_pytz_timezone(col.dtype.tz)
380 if normalized_tz is not col.dtype.tz:
381 df.isetitem(i, col.dt.tz_convert(normalized_tz))
383 df.index = _normalize_timezone_index(df.index)
384 df.columns = _normalize_timezone_index(df.columns)
386 return df