Skip to content

Commit ae0a92a

Browse files
mcocdawcjorisvandenbossche
authored andcommitted
ENH: to_string/to_latex now accept list-like header arg for overwriting column names (pandas-dev#15548)
closes pandas-dev#15536
1 parent a1d3ff3 commit ae0a92a

File tree

5 files changed

+84
-16
lines changed

5 files changed

+84
-16
lines changed

doc/source/whatsnew/v0.20.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ Other enhancements
227227
- ``pd.TimedeltaIndex`` now has a custom datetick formatter specifically designed for nanosecond level precision (:issue:`8711`)
228228
- ``pd.types.concat.union_categoricals`` gained the ``ignore_ordered`` argument to allow ignoring the ordered attribute of unioned categoricals (:issue:`13410`). See the :ref:`categorical union docs <categorical.union>` for more information.
229229
- ``pandas.io.json.json_normalize()`` with an empty ``list`` will return an empty ``DataFrame`` (:issue:`15534`)
230+
- ``pd.DataFrame.to_latex`` and ``pd.DataFrame.to_string`` now allow optional header aliases. (:issue:`15536`)
230231

231232
.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations
232233

pandas/core/frame.py

+5
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,8 @@ def to_feather(self, fname):
15161516
from pandas.io.feather_format import to_feather
15171517
to_feather(self, fname)
15181518

1519+
@Substitution(header='Write out column names. If a list of string is given, \
1520+
it is assumed to be aliases for the column names')
15191521
@Appender(fmt.docstring_to_string, indents=1)
15201522
def to_string(self, buf=None, columns=None, col_space=None, header=True,
15211523
index=True, na_rep='NaN', formatters=None, float_format=None,
@@ -1543,6 +1545,7 @@ def to_string(self, buf=None, columns=None, col_space=None, header=True,
15431545
result = formatter.buf.getvalue()
15441546
return result
15451547

1548+
@Substitution(header='whether to print column labels, default True')
15461549
@Appender(fmt.docstring_to_string, indents=1)
15471550
def to_html(self, buf=None, columns=None, col_space=None, header=True,
15481551
index=True, na_rep='NaN', formatters=None, float_format=None,
@@ -1596,6 +1599,8 @@ def to_html(self, buf=None, columns=None, col_space=None, header=True,
15961599
if buf is None:
15971600
return formatter.buf.getvalue()
15981601

1602+
@Substitution(header='Write out column names. If a list of string is given, \
1603+
it is assumed to be aliases for the column names.')
15991604
@Appender(fmt.common_docstring + fmt.return_docstring, indents=1)
16001605
def to_latex(self, buf=None, columns=None, col_space=None, header=True,
16011606
index=True, na_rep='NaN', formatters=None, float_format=None,

pandas/formats/format.py

+22-16
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
is_float,
2121
is_numeric_dtype,
2222
is_datetime64_dtype,
23-
is_timedelta64_dtype)
23+
is_timedelta64_dtype,
24+
is_list_like)
2425
from pandas.types.generic import ABCSparseArray
25-
2626
from pandas.core.base import PandasObject
2727
from pandas.core.index import Index, MultiIndex, _ensure_index
2828
from pandas import compat
@@ -54,7 +54,7 @@
5454
col_space : int, optional
5555
the minimum width of each column
5656
header : bool, optional
57-
whether to print column labels, default True
57+
%(header)s
5858
index : bool, optional
5959
whether to print index (row) labels, default True
6060
na_rep : string, optional
@@ -488,32 +488,38 @@ def _to_str_columns(self):
488488
# may include levels names also
489489

490490
str_index = self._get_formatted_index(frame)
491-
str_columns = self._get_formatted_column_labels(frame)
492491

493-
if self.header:
492+
if not is_list_like(self.header) and not self.header:
494493
stringified = []
495494
for i, c in enumerate(frame):
496-
cheader = str_columns[i]
497-
max_colwidth = max(self.col_space or 0, *(self.adj.len(x)
498-
for x in cheader))
499495
fmt_values = self._format_col(i)
500496
fmt_values = _make_fixed_width(fmt_values, self.justify,
501-
minimum=max_colwidth,
497+
minimum=(self.col_space or 0),
502498
adj=self.adj)
503-
504-
max_len = max(np.max([self.adj.len(x) for x in fmt_values]),
505-
max_colwidth)
506-
cheader = self.adj.justify(cheader, max_len, mode=self.justify)
507-
stringified.append(cheader + fmt_values)
499+
stringified.append(fmt_values)
508500
else:
501+
if is_list_like(self.header):
502+
if len(self.header) != len(self.columns):
503+
raise ValueError(('Writing %d cols but got %d aliases'
504+
% (len(self.columns), len(self.header))))
505+
str_columns = [[label] for label in self.header]
506+
else:
507+
str_columns = self._get_formatted_column_labels(frame)
508+
509509
stringified = []
510510
for i, c in enumerate(frame):
511+
cheader = str_columns[i]
512+
header_colwidth = max(self.col_space or 0,
513+
*(self.adj.len(x) for x in cheader))
511514
fmt_values = self._format_col(i)
512515
fmt_values = _make_fixed_width(fmt_values, self.justify,
513-
minimum=(self.col_space or 0),
516+
minimum=header_colwidth,
514517
adj=self.adj)
515518

516-
stringified.append(fmt_values)
519+
max_len = max(np.max([self.adj.len(x) for x in fmt_values]),
520+
header_colwidth)
521+
cheader = self.adj.justify(cheader, max_len, mode=self.justify)
522+
stringified.append(cheader + fmt_values)
517523

518524
strcols = stringified
519525
if self.index:

pandas/tests/formats/test_format.py

+11
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,17 @@ def test_to_string_no_header(self):
11251125

11261126
self.assertEqual(df_s, expected)
11271127

1128+
def test_to_string_specified_header(self):
1129+
df = DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
1130+
1131+
df_s = df.to_string(header=['X', 'Y'])
1132+
expected = ' X Y\n0 1 4\n1 2 5\n2 3 6'
1133+
1134+
self.assertEqual(df_s, expected)
1135+
1136+
with tm.assertRaises(ValueError):
1137+
df.to_string(header=['X'])
1138+
11281139
def test_to_string_no_index(self):
11291140
df = DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
11301141

pandas/tests/formats/test_to_latex.py

+45
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,51 @@ def test_to_latex_no_header(self):
428428

429429
assert withoutindex_result == withoutindex_expected
430430

431+
def test_to_latex_specified_header(self):
432+
# GH 7124
433+
df = DataFrame({'a': [1, 2], 'b': ['b1', 'b2']})
434+
withindex_result = df.to_latex(header=['AA', 'BB'])
435+
withindex_expected = r"""\begin{tabular}{lrl}
436+
\toprule
437+
{} & AA & BB \\
438+
\midrule
439+
0 & 1 & b1 \\
440+
1 & 2 & b2 \\
441+
\bottomrule
442+
\end{tabular}
443+
"""
444+
445+
assert withindex_result == withindex_expected
446+
447+
withoutindex_result = df.to_latex(header=['AA', 'BB'], index=False)
448+
withoutindex_expected = r"""\begin{tabular}{rl}
449+
\toprule
450+
AA & BB \\
451+
\midrule
452+
1 & b1 \\
453+
2 & b2 \\
454+
\bottomrule
455+
\end{tabular}
456+
"""
457+
458+
assert withoutindex_result == withoutindex_expected
459+
460+
withoutescape_result = df.to_latex(header=['$A$', '$B$'], escape=False)
461+
withoutescape_expected = r"""\begin{tabular}{lrl}
462+
\toprule
463+
{} & $A$ & $B$ \\
464+
\midrule
465+
0 & 1 & b1 \\
466+
1 & 2 & b2 \\
467+
\bottomrule
468+
\end{tabular}
469+
"""
470+
471+
assert withoutescape_result == withoutescape_expected
472+
473+
with tm.assertRaises(ValueError):
474+
df.to_latex(header=['A'])
475+
431476
def test_to_latex_decimal(self, frame):
432477
# GH 12031
433478
frame.to_latex()

0 commit comments

Comments
 (0)