Skip to content

Commit eb5f8cf

Browse files
authored
Merge branch 'main' into 60237
2 parents 6c99f92 + 45aa7a5 commit eb5f8cf

30 files changed

+158
-145
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,8 @@ I/O
703703
- Bug in :meth:`read_stata` raising ``KeyError`` when input file is stored in big-endian format and contains strL data. (:issue:`58638`)
704704
- Bug in :meth:`read_stata` where extreme value integers were incorrectly interpreted as missing for format versions 111 and prior (:issue:`58130`)
705705
- Bug in :meth:`read_stata` where the missing code for double was not recognised for format versions 105 and prior (:issue:`58149`)
706+
- Bug in :meth:`set_option` where setting the pandas option ``display.html.use_mathjax`` to ``False`` has no effect (:issue:`59884`)
707+
- Bug in :meth:`to_excel` where :class:`MultiIndex` columns would be merged to a single row when ``merge_cells=False`` is passed (:issue:`60274`)
706708

707709
Period
708710
^^^^^^

pandas/core/dtypes/cast.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,7 @@ def convert_dtypes(
11621162

11631163
def maybe_infer_to_datetimelike(
11641164
value: npt.NDArray[np.object_],
1165+
convert_to_nullable_dtype: bool = False,
11651166
) -> np.ndarray | DatetimeArray | TimedeltaArray | PeriodArray | IntervalArray:
11661167
"""
11671168
we might have a array (or single object) that is datetime like,
@@ -1199,6 +1200,7 @@ def maybe_infer_to_datetimelike(
11991200
# numpy would have done it for us.
12001201
convert_numeric=False,
12011202
convert_non_numeric=True,
1203+
convert_to_nullable_dtype=convert_to_nullable_dtype,
12021204
dtype_if_all_nat=np.dtype("M8[s]"),
12031205
)
12041206

pandas/core/frame.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,8 +2115,8 @@ def from_records(
21152115
"""
21162116
Convert structured or record ndarray to DataFrame.
21172117
2118-
Creates a DataFrame object from a structured ndarray, sequence of
2119-
tuples or dicts, or DataFrame.
2118+
Creates a DataFrame object from a structured ndarray, or sequence of
2119+
tuples or dicts.
21202120
21212121
Parameters
21222122
----------

pandas/core/generic.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,8 +2211,9 @@ def to_excel(
22112211
via the options ``io.excel.xlsx.writer`` or
22122212
``io.excel.xlsm.writer``.
22132213
2214-
merge_cells : bool, default True
2215-
Write MultiIndex and Hierarchical Rows as merged cells.
2214+
merge_cells : bool or 'columns', default False
2215+
If True, write MultiIndex index and columns as merged cells.
2216+
If 'columns', merge MultiIndex column cells only.
22162217
{encoding_parameter}
22172218
inf_rep : str, default 'inf'
22182219
Representation for infinity (there is no native representation for

pandas/core/internals/blocks.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
PeriodArray,
109109
TimedeltaArray,
110110
)
111+
from pandas.core.arrays.string_ import StringDtype
111112
from pandas.core.base import PandasObject
112113
import pandas.core.common as com
113114
from pandas.core.computation import expressions
@@ -1336,7 +1337,7 @@ def fillna(
13361337
return [self.copy(deep=False)]
13371338

13381339
if limit is not None:
1339-
mask[mask.cumsum(self.ndim - 1) > limit] = False
1340+
mask[mask.cumsum(self.values.ndim - 1) > limit] = False
13401341

13411342
if inplace:
13421343
nbs = self.putmask(mask.T, value)
@@ -1684,7 +1685,7 @@ def where(self, other, cond) -> list[Block]:
16841685
res_values = arr._where(cond, other).T
16851686
except (ValueError, TypeError):
16861687
if self.ndim == 1 or self.shape[0] == 1:
1687-
if isinstance(self.dtype, IntervalDtype):
1688+
if isinstance(self.dtype, (IntervalDtype, StringDtype)):
16881689
# TestSetitemFloatIntervalWithIntIntervalValues
16891690
blk = self.coerce_to_target_dtype(orig_other, raise_on_upcast=False)
16901691
return blk.where(orig_other, orig_cond)
@@ -1854,9 +1855,9 @@ def fillna(
18541855
limit: int | None = None,
18551856
inplace: bool = False,
18561857
) -> list[Block]:
1857-
if isinstance(self.dtype, IntervalDtype):
1858+
if isinstance(self.dtype, (IntervalDtype, StringDtype)):
18581859
# Block.fillna handles coercion (test_fillna_interval)
1859-
if limit is not None:
1860+
if isinstance(self.dtype, IntervalDtype) and limit is not None:
18601861
raise ValueError("limit must be None")
18611862
return super().fillna(
18621863
value=value,

pandas/core/internals/construction.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,8 +966,9 @@ def convert(arr):
966966
if dtype is None:
967967
if arr.dtype == np.dtype("O"):
968968
# i.e. maybe_convert_objects didn't convert
969-
arr = maybe_infer_to_datetimelike(arr)
970-
if dtype_backend != "numpy" and arr.dtype == np.dtype("O"):
969+
convert_to_nullable_dtype = dtype_backend != "numpy"
970+
arr = maybe_infer_to_datetimelike(arr, convert_to_nullable_dtype)
971+
if convert_to_nullable_dtype and arr.dtype == np.dtype("O"):
971972
new_dtype = StringDtype()
972973
arr_cls = new_dtype.construct_array_type()
973974
arr = arr_cls._from_sequence(arr, dtype=new_dtype)

pandas/io/formats/excel.py

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
CSSWarning,
4949
)
5050
from pandas.io.formats.format import get_level_lengths
51-
from pandas.io.formats.printing import pprint_thing
5251

5352
if TYPE_CHECKING:
5453
from pandas._typing import (
@@ -620,61 +619,43 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
620619
return
621620

622621
columns = self.columns
623-
level_strs = columns._format_multi(
624-
sparsify=self.merge_cells in {True, "columns"}, include_names=False
625-
)
622+
merge_columns = self.merge_cells in {True, "columns"}
623+
level_strs = columns._format_multi(sparsify=merge_columns, include_names=False)
626624
level_lengths = get_level_lengths(level_strs)
627625
coloffset = 0
628626
lnum = 0
629627

630628
if self.index and isinstance(self.df.index, MultiIndex):
631629
coloffset = self.df.index.nlevels - 1
632630

633-
if self.merge_cells in {True, "columns"}:
634-
# Format multi-index as a merged cells.
635-
for lnum, name in enumerate(columns.names):
636-
yield ExcelCell(
637-
row=lnum,
638-
col=coloffset,
639-
val=name,
640-
style=None,
641-
)
631+
for lnum, name in enumerate(columns.names):
632+
yield ExcelCell(
633+
row=lnum,
634+
col=coloffset,
635+
val=name,
636+
style=None,
637+
)
642638

643-
for lnum, (spans, levels, level_codes) in enumerate(
644-
zip(level_lengths, columns.levels, columns.codes)
645-
):
646-
values = levels.take(level_codes)
647-
for i, span_val in spans.items():
648-
mergestart, mergeend = None, None
649-
if span_val > 1:
650-
mergestart, mergeend = lnum, coloffset + i + span_val
651-
yield CssExcelCell(
652-
row=lnum,
653-
col=coloffset + i + 1,
654-
val=values[i],
655-
style=None,
656-
css_styles=getattr(self.styler, "ctx_columns", None),
657-
css_row=lnum,
658-
css_col=i,
659-
css_converter=self.style_converter,
660-
mergestart=mergestart,
661-
mergeend=mergeend,
662-
)
663-
else:
664-
# Format in legacy format with dots to indicate levels.
665-
for i, values in enumerate(zip(*level_strs)):
666-
v = ".".join(map(pprint_thing, values))
639+
for lnum, (spans, levels, level_codes) in enumerate(
640+
zip(level_lengths, columns.levels, columns.codes)
641+
):
642+
values = levels.take(level_codes)
643+
for i, span_val in spans.items():
644+
mergestart, mergeend = None, None
645+
if merge_columns and span_val > 1:
646+
mergestart, mergeend = lnum, coloffset + i + span_val
667647
yield CssExcelCell(
668648
row=lnum,
669649
col=coloffset + i + 1,
670-
val=v,
650+
val=values[i],
671651
style=None,
672652
css_styles=getattr(self.styler, "ctx_columns", None),
673653
css_row=lnum,
674654
css_col=i,
675655
css_converter=self.style_converter,
656+
mergestart=mergestart,
657+
mergeend=mergeend,
676658
)
677-
678659
self.rowcounter = lnum
679660

680661
def _format_header_regular(self) -> Iterable[ExcelCell]:
@@ -798,11 +779,8 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
798779

799780
# MultiIndex columns require an extra row
800781
# with index names (blank if None) for
801-
# unambiguous round-trip, unless not merging,
802-
# in which case the names all go on one row Issue #11328
803-
if isinstance(self.columns, MultiIndex) and (
804-
self.merge_cells in {True, "columns"}
805-
):
782+
# unambiguous round-trip, Issue #11328
783+
if isinstance(self.columns, MultiIndex):
806784
self.rowcounter += 1
807785

808786
# if index labels are not empty go ahead and dump

pandas/io/formats/html.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def _write_table(self, indent: int = 0) -> None:
241241
use_mathjax = get_option("display.html.use_mathjax")
242242
if not use_mathjax:
243243
_classes.append("tex2jax_ignore")
244+
_classes.append("mathjax_ignore")
244245
if self.classes is not None:
245246
if isinstance(self.classes, str):
246247
self.classes = self.classes.split()

pandas/io/formats/style_render.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,11 @@ def _translate(
366366
if not get_option("styler.html.mathjax"):
367367
table_attr = table_attr or ""
368368
if 'class="' in table_attr:
369-
table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
369+
table_attr = table_attr.replace(
370+
'class="', 'class="tex2jax_ignore mathjax_ignore '
371+
)
370372
else:
371-
table_attr += ' class="tex2jax_ignore"'
373+
table_attr += ' class="tex2jax_ignore mathjax_ignore"'
372374
d.update({"table_attributes": table_attr})
373375

374376
if self.tooltips:

pandas/io/sql.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
from pandas.core.dtypes.common import (
4646
is_dict_like,
4747
is_list_like,
48+
is_object_dtype,
49+
is_string_dtype,
4850
)
4951
from pandas.core.dtypes.dtypes import (
5052
ArrowDtype,
@@ -58,6 +60,7 @@
5860
Series,
5961
)
6062
from pandas.core.arrays import ArrowExtensionArray
63+
from pandas.core.arrays.string_ import StringDtype
6164
from pandas.core.base import PandasObject
6265
import pandas.core.common as com
6366
from pandas.core.common import maybe_make_list
@@ -1316,7 +1319,12 @@ def _harmonize_columns(
13161319
elif dtype_backend == "numpy" and col_type is float:
13171320
# floats support NA, can always convert!
13181321
self.frame[col_name] = df_col.astype(col_type)
1319-
1322+
elif (
1323+
using_string_dtype()
1324+
and is_string_dtype(col_type)
1325+
and is_object_dtype(self.frame[col_name])
1326+
):
1327+
self.frame[col_name] = df_col.astype(col_type)
13201328
elif dtype_backend == "numpy" and len(df_col) == df_col.count():
13211329
# No NA values, can convert ints and bools
13221330
if col_type is np.dtype("int64") or col_type is bool:
@@ -1403,6 +1411,7 @@ def _get_dtype(self, sqltype):
14031411
DateTime,
14041412
Float,
14051413
Integer,
1414+
String,
14061415
)
14071416

14081417
if isinstance(sqltype, Float):
@@ -1422,6 +1431,10 @@ def _get_dtype(self, sqltype):
14221431
return date
14231432
elif isinstance(sqltype, Boolean):
14241433
return bool
1434+
elif isinstance(sqltype, String):
1435+
if using_string_dtype():
1436+
return StringDtype(na_value=np.nan)
1437+
14251438
return object
14261439

14271440

@@ -2205,7 +2218,7 @@ def read_table(
22052218
elif using_string_dtype():
22062219
from pandas.io._util import arrow_string_types_mapper
22072220

2208-
arrow_string_types_mapper()
2221+
mapping = arrow_string_types_mapper()
22092222
else:
22102223
mapping = None
22112224

@@ -2286,6 +2299,10 @@ def read_query(
22862299
from pandas.io._util import _arrow_dtype_mapping
22872300

22882301
mapping = _arrow_dtype_mapping().get
2302+
elif using_string_dtype():
2303+
from pandas.io._util import arrow_string_types_mapper
2304+
2305+
mapping = arrow_string_types_mapper()
22892306
else:
22902307
mapping = None
22912308

0 commit comments

Comments
 (0)