Best way to construct a semantic HTML table?
I can't fault your tags at all. The only problem is that due to your table being quite complex, assistive technology may not be able to figure out the structure of your table.

I was going to recommend using scope but according to W3C there are some inconsistency issues.


  The row and col values of the scope attribute are currently supported
  to a large extent by most current versions of JAWS. However, there are
  still some problems and WindowEyes support for scope is inconsistent.
  The same is true for Japanese versions of these screen readers.
  Versions of JAWS prior to version 5 and older versions of Window-Eyes
  have inconsistent support for scope.


Using scope would look like this, with scope="col" in all &lt;th&gt; elements in the first row and scope="row" for all other &lt;th&gt; elements.

&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan="3" scope="col"&gt;th 1&lt;/th&gt;
            &lt;th scope="col"&gt;th 2&lt;/th&gt;
            &lt;th scope="col"&gt;th 3&lt;/th&gt;
            &lt;th scope="col"&gt;th 4&lt;/th&gt;
        &lt;/tr&gt;            
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th rowspan="8" scope="row"&gt;th 5&lt;/th&gt;
            &lt;th rowspan="4" scope="row"&gt;th 6&lt;/th&gt;
            &lt;th rowspan="2" scope="row"&gt;th 7&lt;/th&gt;
            &lt;td&gt;td 1&lt;/td&gt;
            &lt;td&gt;td 2&lt;/td&gt;
            &lt;td&gt;td 3&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;td 4&lt;/td&gt;
            &lt;td&gt;td 5&lt;/td&gt;
            &lt;td&gt;td 6&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th rowspan="2" scope="row"&gt;th 8&lt;/th&gt;
            &lt;td&gt;td 7&lt;/td&gt;
            &lt;td&gt;td 8&lt;/td&gt;
            &lt;td&gt;td 9&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;td 10&lt;/td&gt;
            &lt;td&gt;td 11&lt;/td&gt;
            &lt;td&gt;td 12&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th rowspan="4" scope="row"&gt;th 9&lt;/th&gt;
            &lt;th rowspan="2" scope="row"&gt;th 10&lt;/th&gt;
            &lt;td&gt;td 13&lt;/td&gt;
            &lt;td&gt;td 14&lt;/td&gt;
            &lt;td&gt;td 15&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;td 16&lt;/td&gt;
            &lt;td&gt;td 17&lt;/td&gt;
            &lt;td&gt;td 18&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th rowspan="2" scope="row"&gt;th 11&lt;/th&gt;
            &lt;td&gt;td 19&lt;/td&gt;
            &lt;td&gt;td 20&lt;/td&gt;
            &lt;td&gt;td 21&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;td 22&lt;/td&gt;
            &lt;td&gt;td 23&lt;/td&gt;
            &lt;td&gt;td 24&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;


JSFiddle for scope method

This is W3C's recommendation:


  At the current time, those who want to ensure consistent support
  across Assistive Technologies for tables where the headers are not in
  the first row/column may want to use the technique for complex tables
  H43: Using id and headers attributes to associate data cells with
  header cells in data tables. For simple tables that have headers in
  the first column or row we recommend the use of the th and td
  elements.


Let me just start off by saying this is a complete pain in order to be completely accessibly and scope will probably be fine in most cases. Here is the table using the id/header method (shudder):

&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan="3" id="th1"&gt;th 1&lt;/th&gt;
            &lt;th id="th2"&gt;th 2&lt;/th&gt;
            &lt;th id="th3"&gt;th 3&lt;/th&gt;
            &lt;th id="th4"&gt;th 4&lt;/th&gt;
        &lt;/tr&gt;            
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th rowspan="8" id="th5" headers="th1"&gt;th 5&lt;/th&gt;
            &lt;th rowspan="4" id="th6" headers="th1 th5"&gt;th 6&lt;/th&gt;
            &lt;th rowspan="2" id="th7" headers="th1 th5 th6"&gt;th 7&lt;/th&gt;
            &lt;td headers="th2 th5 th6 th7"&gt;td 1&lt;/td&gt;
            &lt;td headers="th3 th5 th6 th7"&gt;td 2&lt;/td&gt;
            &lt;td headers="th4 th5 th6 th7"&gt;td 3&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td headers="th2 th5 th6 th7"&gt;td 4&lt;/td&gt;
            &lt;td headers="th3 th5 th6 th7"&gt;td 5&lt;/td&gt;
            &lt;td headers="th4 th5 th6 th7"&gt;td 6&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th rowspan="2" id="th8" headers="th1 th5 th6"&gt;th 8&lt;/th&gt;
            &lt;td headers="th2 th5 th6 th8"&gt;td 7&lt;/td&gt;
            &lt;td headers="th3 th5 th6 th8"&gt;td 8&lt;/td&gt;
            &lt;td headers="th4 th5 th6 th8"&gt;td 9&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td headers="th2 th5 th6 th8"&gt;td 10&lt;/td&gt;
            &lt;td headers="th3 th5 th6 th8"&gt;td 11&lt;/td&gt;
            &lt;td headers="th4 th5 th6 th8"&gt;td 12&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th rowspan="4" id="th9" headers="th1 th5"&gt;th 9&lt;/th&gt;
            &lt;th rowspan="2" id="th10" headers="th1 th5 th9"&gt;th 10&lt;/th&gt;
            &lt;td headers="th2 th5 th9 th10"&gt;td 13&lt;/td&gt;
            &lt;td headers="th3 th5 th9 th10"&gt;td 14&lt;/td&gt;
            &lt;td headers="th4 th5 th9 th10"&gt;td 15&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td headers="th2 th5 th9 th10"&gt;td 16&lt;/td&gt;
            &lt;td headers="th3 th5 th9 th10"&gt;td 17&lt;/td&gt;
            &lt;td headers="th4 th5 th9 th10"&gt;td 18&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th rowspan="2" id="th11" headers="th1 th5 th9"&gt;th 11&lt;/th&gt;
            &lt;td headers="th2 th5 th9 th11"&gt;td 19&lt;/td&gt;
            &lt;td headers="th3 th5 th9 th11"&gt;td 20&lt;/td&gt;
            &lt;td headers="th4 th5 th9 th11"&gt;td 21&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td headers="th2 th5 th9 th11"&gt;td 22&lt;/td&gt;
            &lt;td headers="th3 th5 th9 th11"&gt;td 23&lt;/td&gt;
            &lt;td headers="th4 th5 th9 th11"&gt;td 24&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;


JSFiddle for id/headers

What is comes down to in the end in my opinion is an accessible solution that is also maintainable in the back end. Realistically JAWS and WindowEyes will eventually fix up their bugs and using scope will be fine even for tables as complex as this. Unless you expect many users to use assistive technologies and require strict adherence to WCAG, go with scope. Markup needs to be beautiful too :)

There is also the &lt;colgroup&gt; element which is relevant but support for it isn't great currently.