Repository: pandas-dev/pandas

Issue:
to_html formatter not called for float values in a mixed-type column
#### When column x has float64 type, the custom formatter is applied to every value

```
t = pd.DataFrame([[0], [10.4], [3]], columns=['x'])
display(HTML(t.to_html(formatters=dict(x='xxx'.format))))

   x
------
0  xxx
1  xxx
2  xxx
```
#### But when column x is mixed type, the formatter isn't called for (only) floating point values

```
t = pd.DataFrame([['a'], [0], [10.4], [3]], columns=['x'])
display(HTML(t.to_html(formatters=dict(x='xxx'.format))))

   x
------
0  xxx
1  xxx
2  10.4
3  xxx
```
#### output of `pd.show_versions()`
## INSTALLED VERSIONS

commit: None
python: 2.7.11.final.0
python-bits: 64
OS: Darwin
OS-release: 14.5.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

pandas: 0.18.0
nose: None
pip: 1.5.4
setuptools: 20.7.0
Cython: None
numpy: 1.11.0
scipy: 0.17.0
statsmodels: None
xarray: None
IPython: 4.1.2
sphinx: None
patsy: None
dateutil: 2.4.2
pytz: 2016.3
blosc: None
bottleneck: None
tables: 3.2.2
numexpr: 2.5.2
matplotlib: 1.5.1
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
httplib2: None
apiclient: None
sqlalchemy: 1.0.12
pymysql: None
psycopg2: None
jinja2: None
boto: None



Discussion:
what exactly are you expecting here?

I'm expecting it to call my format function for each value in the column.  It doesn't seem to call the formatter at all.  Obviously `'xxx'.format` is a stupid example - but it illustrates that it's not applying the function to the 10.4 value when it occurs in a mixed-type column.  A more realistic formatter would be something like `lambda x: x if type(x) is str else '${:,.0f}'.format(x)`  which displays:

```
    x
0   a
1   $0
2   10.4
3   $3
```

when I'd expect $10 for the 3rd row.

how so? show an expected result (with and w/o object columns)

```
def f(x):
    return x if type(x) is str else '${:,.0f}'.format(x)

t = pd.DataFrame([['a'], [0], [10.4], [3]], columns=['x'])
display(HTML(t.to_html(formatters=dict(x=f))))
```

produces

```
    x
0   a
1   $0
2   10.4
3   $3
```

instead of what I expected:

```
    x
0   a
1   $0
2   $10
3   $3
```

or am I missing something?  (the 'xxx'.format in my example was just to illustrate that it doesn't call my function at all for the 10.4 value)

If instead you create the data frame without the 'a' row, so the column type is float64, you get the expected result:

```
t = pd.DataFrame([[0], [10.4], [3]], columns=['x'])
display(HTML(t.to_html(formatters=dict(x=f))))

    x
0   $0
1   $10
2   $3
```


This is still an issue in 0.21.1

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.2.final.0
python-bits: 64
OS: Linux
OS-release: 4.10.0-42-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.21.1
pytest: 3.0.7
pip: 9.0.1
setuptools: 36.0.1
Cython: None
numpy: 1.13.3
scipy: 1.0.0
pyarrow: None
xarray: None
IPython: 6.1.0
sphinx: 1.6.3
patsy: None
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 1.5.3
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: 4.5.3
html5lib: 0.999999999
sqlalchemy: 1.1.4
pymysql: 0.7.11.None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
@jobevers hence the open status
Also having problems with formatters

Resolving patch:
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -981,6 +981,17 @@ def _format(x):
         if leading_space is None:
             leading_space = is_float_type.any()
 
+        if leading_space is False:
+            # False specifically, so that the default is
+            # to include a space if we get here.
+            tpl = '{v}'
+        else:
+            tpl = ' {v}'
+
+        # shortcut
+        if self.formatter is not None:
+            return [tpl.format(v=self.formatter(x)) for x in self.values]
+
         fmt_values = []
         for i, v in enumerate(vals):
             if not is_float_type[i] and leading_space:
@@ -988,12 +999,6 @@ def _format(x):
             elif is_float_type[i]:
                 fmt_values.append(float_format(v))
             else:
-                if leading_space is False:
-                    # False specifically, so that the default is
-                    # to include a space if we get here.
-                    tpl = '{v}'
-                else:
-                    tpl = ' {v}'
                 fmt_values.append(tpl.format(v=_format(v)))
 
         return fmt_values


Test patch:
diff --git a/pandas/tests/io/formats/data/html/gh13021_expected_output.html b/pandas/tests/io/formats/data/html/gh13021_expected_output.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/gh13021_expected_output.html
@@ -0,0 +1,26 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>x</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>$0</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>$10</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>$3</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py
--- a/pandas/tests/io/formats/test_to_html.py
+++ b/pandas/tests/io/formats/test_to_html.py
@@ -635,6 +635,17 @@ def test_to_html_invalid_classes_type(classes):
         df.to_html(classes=classes)
 
 
+def test_to_html_formatters_object_type(datapath):
+    # GH 13021
+    def f(x):
+        return x if isinstance(x, str) else '${:,.0f}'.format(x)
+
+    df = pd.DataFrame([['a'], [0], [10.4], [3]], columns=['x'])
+    result = df.to_html(formatters=dict(x=f))
+    expected = expected_html(datapath, 'gh13021_expected_output')
+    assert result == expected
+
+
 def test_to_html_round_column_headers():
     # GH 17280
     df = DataFrame([1], columns=[0.55555])