Skip to content

Commit 24d9509

Browse files
cbertinatojreback
authored andcommitted
BUG: Interval and Timedelta unsupported when writing DataFrame to excel (#19244)
1 parent 5f79123 commit 24d9509

File tree

3 files changed

+75
-8
lines changed

3 files changed

+75
-8
lines changed

doc/source/whatsnew/v0.23.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ I/O
490490
- Bug in :func:`DataFrame.to_latex()` where pairs of braces meant to serve as invisible placeholders were escaped (:issue:`18667`)
491491
- Bug in :func:`read_json` where large numeric values were causing an ``OverflowError`` (:issue:`18842`)
492492
- Bug in :func:`DataFrame.to_parquet` where an exception was raised if the write destination is S3 (:issue:`19134`)
493+
- :class:`Interval` now supported in :func:`DataFrame.to_excel` for all Excel file types (:issue:`19242`)
494+
- :class:`Timedelta` now supported in :func:`DataFrame.to_excel` for xls file type (:issue:`19242`, :issue:`9155`)
493495
-
494496

495497
Plotting

pandas/io/excel.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# ---------------------------------------------------------------------
66
# ExcelFile class
7-
from datetime import datetime, date, time, MINYEAR
7+
from datetime import datetime, date, time, MINYEAR, timedelta
88

99
import os
1010
import abc
@@ -21,7 +21,6 @@
2121
from pandas.io.common import (_is_url, _urlopen, _validate_header_arg,
2222
get_filepath_or_buffer, _NA_VALUES,
2323
_stringify_path)
24-
from pandas.core.indexes.period import Period
2524
import pandas._libs.json as json
2625
from pandas.compat import (map, zip, reduce, range, lrange, u, add_metaclass,
2726
string_types, OrderedDict)
@@ -777,17 +776,30 @@ def _pop_header_name(row, index_col):
777776

778777

779778
def _conv_value(val):
780-
# Convert numpy types to Python types for the Excel writers.
779+
""" Convert numpy types to Python types for the Excel writers.
780+
781+
Parameters
782+
----------
783+
val : object
784+
Value to be written into cells
785+
786+
Returns
787+
-------
788+
If val is a numpy int, float, or bool, then the equivalent Python
789+
types are returned. :obj:`datetime`, :obj:`date`, and :obj:`timedelta`
790+
are passed and formatting must be handled in the writer. :obj:`str`
791+
representation is returned for all other types.
792+
"""
781793
if is_integer(val):
782794
val = int(val)
783795
elif is_float(val):
784796
val = float(val)
785797
elif is_bool(val):
786798
val = bool(val)
787-
elif isinstance(val, Period):
788-
val = "{val}".format(val=val)
789-
elif is_list_like(val):
790-
val = str(val)
799+
elif isinstance(val, (datetime, date, timedelta)):
800+
pass
801+
else:
802+
val = compat.to_str(val)
791803

792804
return val
793805

@@ -1460,6 +1472,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
14601472
num_format_str = self.datetime_format
14611473
elif isinstance(cell.val, date):
14621474
num_format_str = self.date_format
1475+
elif isinstance(cell.val, timedelta):
1476+
delta = cell.val
1477+
val = delta.total_seconds() / float(86400)
14631478

14641479
stylekey = json.dumps(cell.style)
14651480
if num_format_str:

pandas/tests/io/test_excel.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import sys
44
import warnings
5-
from datetime import datetime, date, time
5+
from datetime import datetime, date, time, timedelta
66
from distutils.version import LooseVersion
77
from functools import partial
88
from warnings import catch_warnings
@@ -1440,6 +1440,56 @@ def test_excel_date_datetime_format(self):
14401440
# to use df_expected to check the result
14411441
tm.assert_frame_equal(rs2, df_expected)
14421442

1443+
def test_to_excel_interval_no_labels(self):
1444+
# GH19242 - test writing Interval without labels
1445+
_skip_if_no_xlrd()
1446+
1447+
with ensure_clean(self.ext) as path:
1448+
frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)),
1449+
dtype=np.int64)
1450+
expected = frame.copy()
1451+
frame['new'] = pd.cut(frame[0], 10)
1452+
expected['new'] = pd.cut(expected[0], 10).astype(str)
1453+
frame.to_excel(path, 'test1')
1454+
reader = ExcelFile(path)
1455+
recons = read_excel(reader, 'test1')
1456+
tm.assert_frame_equal(expected, recons)
1457+
1458+
def test_to_excel_interval_labels(self):
1459+
# GH19242 - test writing Interval with labels
1460+
_skip_if_no_xlrd()
1461+
1462+
with ensure_clean(self.ext) as path:
1463+
frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)),
1464+
dtype=np.int64)
1465+
expected = frame.copy()
1466+
intervals = pd.cut(frame[0], 10, labels=['A', 'B', 'C', 'D', 'E',
1467+
'F', 'G', 'H', 'I', 'J'])
1468+
frame['new'] = intervals
1469+
expected['new'] = pd.Series(list(intervals))
1470+
frame.to_excel(path, 'test1')
1471+
reader = ExcelFile(path)
1472+
recons = read_excel(reader, 'test1')
1473+
tm.assert_frame_equal(expected, recons)
1474+
1475+
def test_to_excel_timedelta(self):
1476+
# GH 19242, GH9155 - test writing timedelta to xls
1477+
_skip_if_no_xlrd()
1478+
1479+
with ensure_clean('.xls') as path:
1480+
frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)),
1481+
columns=['A'],
1482+
dtype=np.int64
1483+
)
1484+
expected = frame.copy()
1485+
frame['new'] = frame['A'].apply(lambda x: timedelta(seconds=x))
1486+
expected['new'] = expected['A'].apply(
1487+
lambda x: timedelta(seconds=x).total_seconds() / float(86400))
1488+
frame.to_excel(path, 'test1')
1489+
reader = ExcelFile(path)
1490+
recons = read_excel(reader, 'test1')
1491+
tm.assert_frame_equal(expected, recons)
1492+
14431493
def test_to_excel_periodindex(self):
14441494
_skip_if_no_xlrd()
14451495

0 commit comments

Comments
 (0)