Repository: pandas-dev/pandas
Pull request: BUG: to_html() with formatters=<list> and max_cols fixed
- [x] closes #25955
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

@hugoecarl
@guipleite


Problem statement:
bug in to_html() with formatters=<list> and max_cols
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd
import pandas.util.testing as tm
df = tm.makeDataFrame()[:5]
fmt = lambda x: str(x)+'_mod'
formatters=[fmt,fmt,None,None]
print(df.to_html(formatters=formatters))
print(df.to_html(formatters=formatters, max_cols=3))
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>A</th>
      <th>B</th>
      <th>C</th>
      <th>D</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>auVRHDVXKo</th>
      <td>0.8480611318869997_mod</td>
      <td>2.4785562034246165_mod</td>
      <td>-1.258639</td>
      <td>1.134326</td>
    </tr>
    <tr>
      <th>4IL8N5I4tk</th>
      <td>-0.4349431395826644_mod</td>
      <td>1.491615558543841_mod</td>
      <td>-1.392170</td>
      <td>-1.119832</td>
    </tr>
    <tr>
      <th>PACjyGDGOG</th>
      <td>-0.8963777799744853_mod</td>
      <td>0.5011278874888623_mod</td>
      <td>-0.475899</td>
      <td>1.041362</td>
    </tr>
    <tr>
      <th>dGFzef3Sbf</th>
      <td>0.2615536705949168_mod</td>
      <td>1.4994356591304039_mod</td>
      <td>0.713050</td>
      <td>-0.613034</td>
    </tr>
    <tr>
      <th>U2mGzTnuUi</th>
      <td>-0.8803411110313392_mod</td>
      <td>-0.5522186462569404_mod</td>
      <td>0.309005</td>
      <td>-1.715071</td>
    </tr>
  </tbody>
</table>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>A</th>
      <th>...</th>
      <th>D</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>auVRHDVXKo</th>
      <td>0.8480611318869997_mod</td>
      <td>...</td>
      <td>1.134326092661258_mod</td>
    </tr>
    <tr>
      <th>4IL8N5I4tk</th>
      <td>-0.4349431395826644_mod</td>
      <td>...</td>
      <td>-1.1198318989339953_mod</td>
    </tr>
    <tr>
      <th>PACjyGDGOG</th>
      <td>-0.8963777799744853_mod</td>
      <td>...</td>
      <td>1.0413616487242832_mod</td>
    </tr>
    <tr>
      <th>dGFzef3Sbf</th>
      <td>0.2615536705949168_mod</td>
      <td>...</td>
      <td>-0.6130344240242487_mod</td>
    </tr>
    <tr>
      <th>U2mGzTnuUi</th>
      <td>-0.8803411110313392_mod</td>
      <td>...</td>
      <td>-1.7150707649504604_mod</td>
    </tr>
  </tbody>
</table>

#### Problem description

When a list of formatters is given, the integer position of the truncated dataframe is used to lookup the formatter.

the same problem also occurs for `to_string()`

#### Expected Output

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>A</th>
      <th>...</th>
      <th>D</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>auVRHDVXKo</th>
      <td>0.8480611318869997_mod</td>
      <td>...</td>
      <td>1.134326</td>
    </tr>
    <tr>
      <th>4IL8N5I4tk</th>
      <td>-0.4349431395826644_mod</td>
      <td>...</td>
      <td>-1.119832</td>
    </tr>
    <tr>
      <th>PACjyGDGOG</th>
      <td>-0.8963777799744853_mod</td>
      <td>...</td>
      <td>1.041362</td>
    </tr>
    <tr>
      <th>dGFzef3Sbf</th>
      <td>0.2615536705949168_mod</td>
      <td>...</td>
      <td>-0.613034</td>
    </tr>
    <tr>
      <th>U2mGzTnuUi</th>
      <td>-0.8803411110313392_mod</td>
      <td>...</td>
      <td>-1.715071</td>
    </tr>
  </tbody>
</table>

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.7.2.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 58 Stepping 9, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.25.0.dev0+348.gb727c6b32
pytest: 4.3.0
pip: 19.0.3
setuptools: 40.6.3
Cython: 0.29.5
numpy: 1.16.2
scipy: 1.2.1
pyarrow: 0.11.1
xarray: 0.11.3
IPython: 7.3.0
sphinx: 1.8.4
patsy: 0.5.1
dateutil: 2.8.0
pytz: 2018.9
blosc: None
bottleneck: 1.2.1
tables: 3.4.4
numexpr: 2.6.9
feather: None
matplotlib: 3.0.2
openpyxl: 2.6.0
xlrd: 1.2.0
xlwt: 1.3.0
xlsxwriter: 1.1.5
lxml.etree: 4.3.1
bs4: 4.7.1
html5lib: 1.0.1
sqlalchemy: 1.2.18
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: 0.2.0
fastparquet: 0.2.1
pandas_gbq: None
pandas_datareader: None
gcsfs: None
</details>

Hints from issue discussion:
> Would this be an issue that I could attempt to work on?

go for it!

> I would be a first time contributor, but none of the good-first-issue posts are available at the moment.

i would recommend #23922

Commit message: BUG: issue-25955 fixed

Patch to review:
diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst
index 58918f2d8c40e..0ca75402c41ca 100644
--- a/doc/source/whatsnew/v1.0.0.rst
+++ b/doc/source/whatsnew/v1.0.0.rst
@@ -83,6 +83,7 @@ Performance improvements
 Bug fixes
 ~~~~~~~~~
 
+- Bug in to_html() when using formatters=<list> and max_cols together fixed (:issue:`25955`)
 
 Categorical
 ^^^^^^^^^^^
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
index 61af935bd8227..6857f971e68c4 100644
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -656,6 +656,10 @@ def _chk_truncate(self) -> None:
                 frame = concat(
                     (frame.iloc[:, :col_num], frame.iloc[:, -col_num:]), axis=1
                 )
+                # truncate formatter
+                if is_list_like(self.formatters) and self.formatters:
+                    truncate_fmt = cast(List[Callable], self.formatters)
+                    self.formatters = truncate_fmt[:col_num] + truncate_fmt[-col_num:]
             self.tr_col_num = col_num
         if truncate_v:
             # cast here since if truncate_v is True, max_rows_adj is not None
diff --git a/pandas/tests/io/formats/data/html/truncate_formatter.html b/pandas/tests/io/formats/data/html/truncate_formatter.html
new file mode 100644
index 0000000000000..71bdf43258234
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/truncate_formatter.html
@@ -0,0 +1,36 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>A</th>
+      <th>...</th>
+      <th>D</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>1_mod</td>
+      <td>...</td>
+      <td>4</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>5_mod</td>
+      <td>...</td>
+      <td>8</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>9_mod</td>
+      <td>...</td>
+      <td>12</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13_mod</td>
+      <td>...</td>
+      <td>16</td>
+    </tr>
+  </tbody>
+</table>
\ No newline at end of file
diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py
index 448e869df950d..2e24f639cb3d9 100644
--- a/pandas/tests/io/formats/test_to_html.py
+++ b/pandas/tests/io/formats/test_to_html.py
@@ -235,6 +235,22 @@ def test_to_html_truncate(datapath):
     assert result == expected
 
 
+def test_to_html_truncate_formatter(datapath):
+    data = [
+        {"A": 1, "B": 2, "C": 3, "D": 4},
+        {"A": 5, "B": 6, "C": 7, "D": 8},
+        {"A": 9, "B": 10, "C": 11, "D": 12},
+        {"A": 13, "B": 14, "C": 15, "D": 16},
+    ]
+
+    df = pd.DataFrame(data)
+    fmt = lambda x: str(x) + "_mod"
+    formatters = [fmt, fmt, None, None]
+    result = df.to_html(formatters=formatters, max_cols=3)
+    expected = expected_html(datapath, "truncate_formatter")
+    assert result == expected
+
+
 @pytest.mark.parametrize(
     "sparsify,expected",
     [(True, "truncate_multi_index"), (False, "truncate_multi_index_sparse_off")],


Code review comments:
diff_hunk: @@ -235,6 +235,22 @@ def test_to_html_truncate(datapath):
     assert result == expected
 
 
+def test_to_html_truncate_formatter(datapath):
+    data = [
+        {"A": 1, "B": 2, "C": 3, "D": 4},
+        {"A": 5, "B": 6, "C": 7, "D": 8},
+        {"A": 9, "B": 10, "C": 11, "D": 12},
+        {"A": 13, "B": 14, "C": 15, "D": 16},
+    ]
+
+    df = pd.DataFrame(data)
path: pandas/tests/io/formats/test_to_html.py
text: @user1:
```suggestion
    df = DataFrame(data)
```

we don't need the pd here.

or maybe `df = DataFrame(np.arange(16).reshape(4, 4) + 1, columns=list("ABCD"))` which would not require a change to the html file.