Pandas df.to_html() should account for colspan and rowspan as needed if you have a multiindex set up. Given the following dataframe,
df = pd.DataFrame([
('A1', 'B1', '1', 'extra'), ('A1', 'B1', '2', 'extra'),
('A1', 'B2', '3', 'extra'), ('A1', 'B2', '4', 'extra'),
('A2', 'B1', '5', 'extra'), ('A2', 'B1', '6', 'extra'),
('A2', 'B2', '7', 'extra'), ('A2', 'B2', '8', 'extra'),
], columns=['A', 'B', 'C', 'D'])
You can set the columns you want as your index:
df.set_index(['A', 'B', 'C'], inplace=True)
df
# D
# A B C
# A1 B1 1 extra
# 2 extra
# B2 3 extra
# 4 extra
# A2 B1 5 extra
# 6 extra
# B2 7 extra
# 8 extra
Then to view it as html, you just call df.to_html(). You can pass in a file name such as df.to_html("output.html") which yields:
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th></th>
<th></th>
<th>D</th>
</tr>
<tr>
<th>A</th>
<th>B</th>
<th>C</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="4" valign="top">A1</th>
<th rowspan="2" valign="top">B1</th>
<th>1</th>
<td>extra</td>
</tr>
<tr>
<th>2</th>
<td>extra</td>
</tr>
<tr>
<th rowspan="2" valign="top">B2</th>
<th>3</th>
<td>extra</td>
</tr>
<tr>
<th>4</th>
<td>extra</td>
</tr>
<tr>
<th rowspan="4" valign="top">A2</th>
<th rowspan="2" valign="top">B1</th>
<th>5</th>
<td>extra</td>
</tr>
<tr>
<th>6</th>
<td>extra</td>
</tr>
<tr>
<th rowspan="2" valign="top">B2</th>
<th>7</th>
<td>extra</td>
</tr>
<tr>
<th>8</th>
<td>extra</td>
</tr>
</tbody>
</table>