{"id":24616066,"url":"https://github.com/jhorzek/tablespam","last_synced_at":"2026-03-02T08:34:24.363Z","repository":{"id":271453122,"uuid":"903522761","full_name":"jhorzek/tablespam","owner":"jhorzek","description":"Export tables from python to Excel, HTML, LaTeX, and RTF the easy way.","archived":false,"fork":false,"pushed_at":"2025-02-02T12:43:50.000Z","size":1318,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-25T00:02:01.943Z","etag":null,"topics":["data-science","excel","html","latex","polars","python","reporting","tables"],"latest_commit_sha":null,"homepage":"https://jhorzek.github.io/tablespam/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jhorzek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-14T20:19:32.000Z","updated_at":"2025-02-02T12:43:19.000Z","dependencies_parsed_at":"2025-02-02T00:23:10.342Z","dependency_job_id":"42224385-2b33-4477-9b4a-707afaae8e4f","html_url":"https://github.com/jhorzek/tablespam","commit_stats":null,"previous_names":["jhorzek/tablespam"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jhorzek/tablespam","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhorzek%2Ftablespam","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhorzek%2Ftablespam/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhorzek%2Ftablespam/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhorzek%2Ftablespam/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jhorzek","download_url":"https://codeload.github.com/jhorzek/tablespam/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhorzek%2Ftablespam/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29995912,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-02T01:47:34.672Z","status":"online","status_checked_at":"2026-03-02T02:00:07.342Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-science","excel","html","latex","polars","python","reporting","tables"],"created_at":"2025-01-24T22:15:44.909Z","updated_at":"2026-03-02T08:34:24.346Z","avatar_url":"https://github.com/jhorzek.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\n\u003c!-- The README.md is generated automatically from README.qmd\n\u0026#10;To render the README.qmd file, the following actions are required:\n\u0026#10;- install quarto from https://quarto.org/docs/get-started/\n- install R and the package reticulate\n- install jupyter notebooks\n- render the README.qmd\n--\u003e\n\n# Tablespam - Simple Tables Made Simple\n\n\u003c!-- badges: start --\u003e\n\n[![Lifecycle:\nexperimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)\n\u003c!-- badges: end --\u003e\n\nThe objective of `tablespam` is to provide a “good enough” approach to\ncreating tables in python. `tablespam` is a port of the R package\n[`tablespan`](https://jhorzek.github.io/tablespan/).\n\n`tablespam` currently builds on the awesome package\n[`great_tables`](https://posit-dev.github.io/great-tables/articles/intro.html),\nwhich allows tables created with `tablespam` to be exported to the\nfollowing formats:\n\n- **Excel** (using\n  [`openpyxl`](https://openpyxl.readthedocs.io/en/stable/))\n- **HTML** (using\n  [`great_tables`](https://posit-dev.github.io/great-tables/articles/intro.html))\n- **LaTeX** (using\n  [`great_tables`](https://posit-dev.github.io/great-tables/articles/intro.html))\n- **RTF** (using\n  [`great_tables`](https://posit-dev.github.io/great-tables/articles/intro.html))\n\n## Installation\n\n`tablespam` is available from pip:\n\n    pip install tablespam\n\nTo install the development version from GitHub, run:\n\n    pip install git+https://github.com/jhorzek/tablespam.git#egg=tablespam\n\n## Introduction\n\nPython has a large set of great packages that allow you to create and\nexport tables that look exactly like you envisioned. However, sometimes\nyou may just need a good-enough table that is easy to create and share\nwith others. This is where `tablespam` can be of help.\n\nLet’s assume that we want to share the following table:\n\n``` python\nimport polars as pl\nfrom tablespam.Data.mtcars import mtcars\n\n# Import mtcars data from R:\ncars = mtcars()\n\nsummarized_table = cars.group_by(['cyl', 'vs'], \n                                  maintain_order=True).agg(\n    [\n        pl.len().alias('N'),\n        pl.col('hp').mean().alias('mean_hp'),\n        pl.col('hp').std().alias('sd_hp'),\n        pl.col('wt').mean().alias('mean_wt'),\n        pl.col('wt').std().alias('sd_wt'),\n    ]\n).sort(pl.col('cyl', 'vs'))\n\nprint(summarized_table)\n```\n\n    shape: (5, 7)\n    ┌─────┬─────┬─────┬────────────┬───────────┬──────────┬──────────┐\n    │ cyl ┆ vs  ┆ N   ┆ mean_hp    ┆ sd_hp     ┆ mean_wt  ┆ sd_wt    │\n    │ --- ┆ --- ┆ --- ┆ ---        ┆ ---       ┆ ---      ┆ ---      │\n    │ i64 ┆ i64 ┆ u32 ┆ f64        ┆ f64       ┆ f64      ┆ f64      │\n    ╞═════╪═════╪═════╪════════════╪═══════════╪══════════╪══════════╡\n    │ 4   ┆ 0   ┆ 1   ┆ 91.0       ┆ null      ┆ 2.14     ┆ null     │\n    │ 4   ┆ 1   ┆ 10  ┆ 81.8       ┆ 21.872357 ┆ 2.3003   ┆ 0.598207 │\n    │ 6   ┆ 0   ┆ 3   ┆ 131.666667 ┆ 37.527767 ┆ 2.755    ┆ 0.12816  │\n    │ 6   ┆ 1   ┆ 4   ┆ 115.25     ┆ 9.17878   ┆ 3.38875  ┆ 0.116216 │\n    │ 8   ┆ 0   ┆ 14  ┆ 209.214286 ┆ 50.976886 ┆ 3.999214 ┆ 0.759405 │\n    └─────┴─────┴─────┴────────────┴───────────┴──────────┴──────────┘\n\n\u003e Note: `tablespam` currently only supports `polars` data frames.\n\nWe don’t want to share the table as is - the variable names are all a\nbit technical and the table could need some spanners summarizing\ncolumns. So, we want to share a table that looks something like this:\n\n    |                   | Horse Power |   Weight  |\n    | Cylinder | Engine | Mean  |  SD | Mean | SD |\n    | -------- | ------ | ----- | --- | ---- | -- |\n    |                   |                         |\n\n`tablespam` allows us to create this table with a single formula.\n\n### Creating a Basic Table\n\nIn `tablespam`, the table headers are defined with a formula inspired by\nR. For example, `\"cyl ~ mean_hp + sd_hp\"` defines a table with `cyl` as\nthe row names and `mean_hp` and `sd_hp` as columns:\n\n``` python\nfrom tablespam import TableSpam\ntbl = TableSpam(data = summarized_table,\n                formula = \"cyl ~ mean_hp + sd_hp\")\nprint(tbl.as_string())\n```\n\n\n    | cyl | mean_hp sd_hp |\n    | --- - ------- ----- |\n    | 4   | 91.0    None  |\n    | 4   | 81.8    21.87 |\n    | 6   | 131.67  37.53 |\n    | ... | ...     ...   |\n\nNote that the row names (`cyl`) are in a separate block to the left.\n\n### Adding Spanners\n\nSpanners are defined using braces and spanner names. For example, the\nfollowing defines a spanner for `mean_hp` and `sd_hp` with the name\n`Horsepower`: `\"cyl ~ (Horsepower = mean_hp + sd_hp)\"`:\n\n``` python\ntbl = TableSpam(data = summarized_table,\n                formula = \"cyl ~ (Horsepower = mean_hp + sd_hp)\")\nprint(tbl.as_string())\n```\n\n\n    |     | Horsepower       |\n    | cyl | mean_hp    sd_hp |\n    | --- - ---------- ----- |\n    | 4   | 91.0       None  |\n    | 4   | 81.8       21.87 |\n    | 6   | 131.67     37.53 |\n    | ... | ...        ...   |\n\nSpanners can also be nested:\n\n``` python\ntbl = TableSpam(data = summarized_table,\n                formula = \"cyl ~ (Horsepower = (Mean = mean_hp) + (SD  = sd_hp))\")\nprint(tbl.as_string())\n```\n\n\n    |     | Horsepower       |\n    |     | Mean       SD    |\n    | cyl | mean_hp    sd_hp |\n    | --- - ---------- ----- |\n    | 4   | 91.0       None  |\n    | 4   | 81.8       21.87 |\n    | 6   | 131.67     37.53 |\n    | ... | ...        ...   |\n\n### Renaming Columns\n\nVariable names in an data frame are often very technical (e.g.,\n`mean_hp` and `sd_hp`). When sharing the table, we may want to replace\nthose names. In the example above, we may want to replace `mean_hp` and\n`sd_hp` with “Mean” and “SD”. In `tablespam` renaming variables is\nachieved with `new_name:old_name`. For example,\n`\"cyl ~ (Horsepower = Mean:mean_hp + SD:sd_hp)\"` renames `mean_hp` to\n`Mean` and `sd_hp` to `SD`:\n\n``` python\ntbl = TableSpam(data = summarized_table,\n                formula = \"cyl ~ (Horsepower = Mean:mean_hp + SD:sd_hp)\")\nprint(tbl.as_string())\n```\n\n\n    |     | Horsepower       |\n    | cyl | Mean       SD    |\n    | --- - ---------- ----- |\n    | 4   | 91.0       None  |\n    | 4   | 81.8       21.87 |\n    | 6   | 131.67     37.53 |\n    | ... | ...        ...   |\n\n### Creating the Full Table\n\nThe combination of row names, spanners, and renaming of variables allows\ncreating the full table:\n\n``` python\ntbl = TableSpam(data = summarized_table,\n                formula = \"\"\"Cylinder:cyl + Engine:vs ~\n                   N +\n                   (`Horse Power` = Mean:mean_hp + SD:sd_hp) +\n                   (`Weight` = Mean:mean_wt + SD:sd_wt)\"\"\",\n                 title = \"Motor Trend Car Road Tests\",\n                 subtitle = \"A table created with tablespam\",\n                 footnote = \"Data from the infamous mtcars data set.\")\nprint(tbl.as_string())\n```\n\n    Motor Trend Car Road Tests\n    A table created with tablespam\n\n    |                 |     Horse Power       Weight      |\n    | Cylinder Engine | N   Mean        SD    Mean   SD   |\n    | -------- ------ - --- ----------- ----- ------ ---- |\n    | 4        0      | 1   91.0        None  2.14   None |\n    | 4        1      | 10  81.8        21.87 2.3    0.6  |\n    | 6        0      | 3   131.67      37.53 2.76   0.13 |\n    | ...      ...    | ... ...         ...   ...    ...  |\n    Data from the infamous mtcars data set.\n\n## Exporting to Excel\n\n\u003e Note: The screenshots below are from the R-package tablespan and may\n\u003e deviate slightly from the actual output. The screenshots will be\n\u003e updated soon.\n\nTables created with `tablespam` can be exported to `openpyxl` workbooks,\nwhich allows saving as .xlsx files.\n\n``` python\n# Translate to openpyxl:\ntbl_xlsx = tbl.as_excel()\n\n# save the table:\n# tbl_xlsx.save(\"my_table.xlsx\")\n```\n\n![](assets/tablespan_example_cars.png)\n\n### Styling\n\nWhile `tablespam` provides limited styling options, some elements can be\nadjusted. For example, we may want to print some elements in bold or\nformat numbers differently. In `tablespam`, styling happens when\ntranslating the table to an `openpyxl` workbook with `as_excel`.\n\n#### Changing the Overall Look\n\nThe easiest way to customize tables is to change the default color\nscheme. The class `XlsxStyles` provides control over most elements in\nthe table, but in many cases `style_color` may be sufficient. The\nfollowing creates a table with teal-colored backgrounds for the title,\nheader, and row names:\n\n``` python\nfrom tablespam.Excel.xlsx_styles import style_color\n\ntbl_xlsx = tbl.as_excel(styles = style_color(primary_color = \"008080\"))\n\n# save the table:\n# tbl_xlsx.save(\"my_table.xlsx\")\n```\n\n![](assets/tablespan_example_cars_color.png)\n\nSimilarly, a dark background can be defined as follows:\n\n``` python\ntbl_xlsx = tbl.as_excel(styles = style_color(primary_color = \"000000\"))\n\n# save the table:\n# tbl_xlsx.save(\"my_table.xlsx\")\n```\n\n#### Formatting Cells\n\nLet’s assume we want all `mean_hp` values with a value $\\geq 100$ to be\nprinted in bold. To this end, we first create a function that takes in a\nsingle openpyxl cell and applies a style to it:\n\n``` python\nimport openpyxl\ndef bold(c):\n    c.font = openpyxl.styles.Font(bold=True)\n```\n\nNext, we have to define a CellStyle for the column `mean_hp`, where we\npass in the index of the rows that should be bold.\n\n\u003e Note: openpyxl uses 1-based indexing!\n\n``` python\n# get the indices:\ngeq_100 = (summarized_table.with_row_index(name=\"index\") \n            .filter(pl.col(\"mean_hp\") \u003e= 100) \n            .select(\"index\") \n            .to_series() \n            .to_list())\n# translate to 1-based index:\ngeq_100 = [i + 1 for i in geq_100]\n```\n\nDefine cell styles:\n\n``` python\nfrom tablespam.Excel.xlsx_styles import XlsxStyles, CellStyle\nstyles=XlsxStyles(\n            cell_styles=[\n                CellStyle(\n                    rows=geq_100,\n                    cols=['mean_hp'],\n                    style=bold,\n                ),\n            ]\n        )\n```\n\nFinally, we pass this style to `as_excel`:\n\n``` python\ntbl_xlsx = tbl.as_excel(styles=styles)\n\n# save the table:\n# tbl_xlsx.save(\"my_table.xlsx\")\n```\n\n![](assets/tablespan_example_cars_styled.png)\n\n#### Formatting Data Types\n\n`tablespan` also allows formatting specific data types. Let’s assume\nthat we want to round all doubles to 3 instead of the default 2 digits.\nTo this end, we use `DataStyle`s, where we specify (1) a function that\nchecks for the data type we want to style (here for doubles) and (2) a\nstyle for all columns that match that style:\n\n``` python\nfrom tablespam.Excel.xlsx_styles import DataStyle\n# Define test: The function will be passed a polars data frame and should\n# check for the data types defined here: \n# https://docs.pola.rs/api/python/stable/reference/datatypes.html\ndef is_double(x: pl.DataFrame) -\u003e bool:\n    return all([tp in [pl.Float32, pl.Float64] for tp in x.dtypes])\n\n# Function that applies our style to a single cell:\ndef three_digits(c):\n    c.number_format = \"0.000\"\n\n# Now we define the data_style. The data_style must be a \n# dict.\ndata_style = {\n    \"double\": DataStyle(test = is_double,\n                         style = three_digits) \n} \ntbl_xlsx = tbl.as_excel(\n        styles=XlsxStyles(\n            data_styles=data_style\n        )\n    )\n# tbl_xlsx.save(\"my_table.xlsx\")\n```\n\n![](assets/tablespan_example_cars_styled_data.png)\n\n## Exporting to HTML, LaTeX, and RTF\n\nTables created with `tablespam` can be exported to `great_tables` which\nallows saving as HTML, LaTeX, or RTF file. To this end, we simply have\nto call `as_gt` on our table:\n\n``` python\n# Translate to gt:\ngt_tbl = tbl.as_gt()\n```\n\n``` python\ngt_tbl.show()\n```\n\n\u003cp align=\"center\"\u003e\n\n\u003cimg src=\"assets/tablespan_example_gt_cars.png\" alt=\"Standard table\" width=\"50%\"\u003e\n\u003c/p\u003e\n\n### Styling Great Tables\n\nThe `great_tables` package provides a wide range of functions to adapt\nthe style of the table created with `as_gt`. For instance, `opt_stylize`\nadds a pre-defined style to the entire table:\n\n``` python\n(gt_tbl\n  .opt_stylize(style = 6,\n               color = 'gray')\n  .show())\n```\n\n\u003cp align=\"center\"\u003e\n\n\u003cimg src=\"assets/tablespan_example_gt_cars_styled.png\" alt=\"Styled table\" width=\"50%\"\u003e\n\u003c/p\u003e\n\nWhen adapting the `great_tables` object, there is an important detail to\nkeep in mind: To ensure that each table spanner has a unique ID,\n`tablespam` will create IDs that differ from the text shown in the\nspanner. To demonstrate this, Let’s assume that we want to add a spanner\nabove `Horse Power` and `Weight`:\n\n``` python\n(gt_tbl\n  .tab_spanner(label = \"New Spanner\", \n               spanners = [\"Horse Power\", \"Weight\"]))\n```\n\n    AssertionError: \n    \u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n    \u001b[0;31mAssertionError\u001b[0m                            Traceback (most recent call last)\n    Cell \u001b[0;32mIn[33], line 2\u001b[0m\n    \u001b[1;32m      1\u001b[0m (\u001b[43mgt_tbl\u001b[49m\n    \u001b[0;32m----\u003e 2\u001b[0m \u001b[43m  \u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtab_spanner\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlabel\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mNew Spanner\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n    \u001b[1;32m      3\u001b[0m \u001b[43m               \u001b[49m\u001b[43mspanners\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mHorse Power\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mWeight\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m)\n\n    File \u001b[0;32m~/Documents/Programming/Python/tablespam/.venv/lib/python3.13/site-packages/great_tables/_spanners.py:165\u001b[0m, in \u001b[0;36mtab_spanner\u001b[0;34m(self, label, columns, spanners, level, id, gather, replace)\u001b[0m\n    \u001b[1;32m    161\u001b[0m \u001b[38;5;66;03m# select spanner ids ----\u001b[39;00m\n    \u001b[1;32m    162\u001b[0m \u001b[38;5;66;03m# TODO: this supports tidyselect\u001b[39;00m\n    \u001b[1;32m    163\u001b[0m \u001b[38;5;66;03m# TODO: could we use something like resolve_vector_l\u001b[39;00m\n    \u001b[1;32m    164\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m spanners \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n    \u001b[0;32m--\u003e 165\u001b[0m     \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28mset\u001b[39m(spanners)\u001b[38;5;241m.\u001b[39missubset(\u001b[38;5;28mset\u001b[39m(crnt_spanner_ids))\n    \u001b[1;32m    166\u001b[0m     spanner_ids \u001b[38;5;241m=\u001b[39m spanners\n    \u001b[1;32m    167\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\n    \u001b[0;31mAssertionError\u001b[0m: \n\nThis will throw an error because the spanner IDs are different from the\nspanner labels.\n\nIn general, the ID of a spanner is given by a concatenation of\n“**BASE_LEVEL**” and the names of all spanners above the current\nspanner. For example, the IDs for Horse Power and Weight are\n“\\_\\_BASE_LEVEL\\_\\_Horse Power” and “\\_\\_BASE_LEVEL\\_\\_Weight”:\n\n``` python\n(gt_tbl\n  .tab_spanner(label = \"New Spanner\", \n                  spanners = [\"__BASE_LEVEL__Horse Power\", \n                               \"__BASE_LEVEL__Weight\"])\n  .show())\n```\n\n\u003cp align=\"center\"\u003e\n\n\u003cimg src=\"assets/tablespan_example_new_spanner.png\" alt=\"Table with additional spanner\" width=\"50%\"\u003e\n\u003c/p\u003e\n\n## Tables without row names\n\nUsing `1` on the left hand side of the formula creates a table without\nrow names. For example, `\"1 ~ (Horsepower = Mean:mean_hp + SD:sd_hp)\"`\ndefines\n\n``` python\ntbl = TableSpam(data = summarized_table,\n                formula = \"1 ~ (Horsepower = Mean:mean_hp + SD:sd_hp)\")\nprint(tbl.as_string())\n```\n\n\n    | Horsepower       |\n    | Mean       SD    |\n    | ---------- ----- |\n    | 91.0       None  |\n    | 81.8       21.87 |\n    | 131.67     37.53 |\n    | ...        ...   |\n\n## References\n\n- great_tables: Iannone R., et al.,\n  https://posit-dev.github.io/great-tables/articles/intro.html\n- tables: Murdoch D (2024). tables: Formula-Driven Table Generation. R\n  package version 0.9.31, \u003chttps://dmurdoch.github.io/tables/\u003e.\n- openpyxl: Gazoni, E. \u0026 Clark C.,\n  https://openpyxl.readthedocs.io/en/stable/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhorzek%2Ftablespam","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjhorzek%2Ftablespam","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhorzek%2Ftablespam/lists"}