mirror of
https://github.com/wassname/greater_tables_project.git
synced 2026-09-12 12:22:43 +08:00
README tidying; testdf massive...
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
|
||||
|
||||
Here's how you subclass.
|
||||
|
||||
|
||||
```python
|
||||
class sGT(GT):
|
||||
"""
|
||||
Example standard GT with Steve House-Style defaults.
|
||||
|
||||
Each application can create its own defaults by subclassing GT
|
||||
in this way.
|
||||
"""
|
||||
|
||||
def __init__(self, df, caption="", guess_years=True, ratio_regex='lr|roe|coc', **kwargs):
|
||||
"""Create Steve House-Style Formatter. Does not handle list of lists input."""
|
||||
if isinstance(df, str):
|
||||
df, aligners_ = GT.md_to_df(df)
|
||||
if 'aligners' not in kwargs:
|
||||
kwargs['aligners'] = aligners_
|
||||
kwargs['show_index'] = False
|
||||
|
||||
nindex = df.index.nlevels
|
||||
ncolumns = df.columns.nlevels
|
||||
if 'ratio_cols' in kwargs:
|
||||
ratio_cols = kwargs['ratio_cols']
|
||||
else:
|
||||
if ratio_regex != '' and ncolumns == 1:
|
||||
ratio_cols = df.filter(regex=ratio_regex).columns.to_list()
|
||||
else:
|
||||
ratio_cols = None
|
||||
|
||||
if guess_years:
|
||||
year_cols = sGT.guess_years(df)
|
||||
else:
|
||||
year_cols = kwargs.get('year_cols', None)
|
||||
|
||||
# rule sizes
|
||||
hrule_widths = (1.5, 1, 0) if nindex > 1 else None
|
||||
vrule_widths = (1.5, 1, 0.5) if ncolumns > 1 else None
|
||||
|
||||
table_hrule_width = 1 if nindex == 1 else 2
|
||||
table_vrule_width = 1 if ncolumns == 1 else (
|
||||
1.5 if ncolumns == 2 else 2)
|
||||
|
||||
# padding
|
||||
nr, nc = df.shape
|
||||
if 'padding_trbl' in kwargs:
|
||||
padding_trbl = kwargs['padding_trbl']
|
||||
else:
|
||||
pad_tb = 4 if nr < 16 else (2 if nr < 25 else 1)
|
||||
pad_lr = 10 if nc < 9 else (5 if nc < 13 else 2)
|
||||
padding_trbl = (pad_tb, pad_lr, pad_tb, pad_lr)
|
||||
|
||||
font_body = 0.9 if nr < 25 else (0.8 if nr < 41 else 0.7)
|
||||
font_caption = np.round(1.1 * font_body, 2)
|
||||
font_head = np.round(1.1 * font_body, 2)
|
||||
|
||||
pef_lower = -3
|
||||
pef_upper = 6
|
||||
pef_precision = 3
|
||||
|
||||
defaults = {
|
||||
'ratio_cols': ratio_cols,
|
||||
'year_cols': year_cols,
|
||||
'default_integer_str': '{x:,.0f}',
|
||||
'default_float_str': '{x:,.3f}',
|
||||
'default_date_str': '%Y-%m-%d',
|
||||
'default_ratio_str': '{x:.1%}',
|
||||
'cast_to_floats': True,
|
||||
'table_hrule_width': table_hrule_width,
|
||||
'table_vrule_width': table_vrule_width,
|
||||
'hrule_widths': hrule_widths,
|
||||
'vrule_widths': vrule_widths,
|
||||
'sparsify': True,
|
||||
'sparsify_columns': True,
|
||||
'padding_trbl': padding_trbl,
|
||||
'font_body': font_body,
|
||||
'font_head': font_head,
|
||||
'font_caption': font_caption,
|
||||
'pef_precision': pef_precision,
|
||||
'pef_lower': pef_lower,
|
||||
'pef_upper': pef_upper,
|
||||
'debug': False
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
super().__init__(df, caption=caption, **defaults)
|
||||
|
||||
@staticmethod
|
||||
def guess_years(df):
|
||||
"""Try to guess which columns (body or index) are years.
|
||||
|
||||
A column is considered a year if:
|
||||
- It is numeric (integer or convertible to integer)
|
||||
- All values are within a reasonable range (e.g., 1800–2100)
|
||||
"""
|
||||
year_columns = []
|
||||
df = df.reset_index(drop=False, col_level=df.columns.nlevels - 1)
|
||||
for i, col in enumerate(df.columns):
|
||||
try:
|
||||
series = pd.to_numeric(df[col], errors='coerce').dropna()
|
||||
if series.dtype.kind in 'iu' and series.between(1800, 2100).all():
|
||||
year_columns.append(col)
|
||||
except Exception:
|
||||
continue
|
||||
return year_columns
|
||||
|
||||
```
|
||||
@@ -1,5 +1,8 @@
|
||||
sort out the variety of readmes... this is the main one
|
||||
|
||||
https://shields.io/badges/read-the-docs
|
||||
|
||||
|
||||
# v 3.0 update
|
||||
|
||||
* config files
|
||||
@@ -195,12 +198,56 @@ More coming soon.
|
||||
|
||||
## Documentation
|
||||
|
||||

|
||||
|
||||
Available on
|
||||
[readthedocs](https://greater-tables-project.readthedocs.io/en/latest).
|
||||
|
||||
## Versions
|
||||
|
||||
### 1.1.1
|
||||
3.0.0
|
||||
-------
|
||||
|
||||
2.0.0
|
||||
------
|
||||
|
||||
1.1.1
|
||||
-------
|
||||
* Added logo, updated docs.
|
||||
|
||||
### 1.1.0
|
||||
1.1.0
|
||||
------
|
||||
|
||||
* added ``formatters`` argument to pass in column specific formatters by name as a number (``n`` converts to ``{x:.nf}``, format string, or function
|
||||
* Added ```tabs`` argument to provide column widths
|
||||
* Added ``equal`` argument to provide hint that column widths should all be equal
|
||||
* Added ``caption_align='center'`` argument to set the caption alignment
|
||||
* Added ``large_ok=False`` argument, if ``False`` providing a dataframe with more than 100 rows throws an error. This function is expensive and is designed for small frames.
|
||||
|
||||
|
||||
1.0.0
|
||||
------
|
||||
|
||||
* Allow input via list of lists, or markdown table
|
||||
* Specify overall float format for whole table
|
||||
* Specify column alingment with 'llrc' style string
|
||||
* ``show_index`` option
|
||||
* Added more tests
|
||||
* Docs updated
|
||||
* Set tabs for width; use of width in HTML format.
|
||||
|
||||
|
||||
0.6.0
|
||||
------
|
||||
|
||||
* Initial release
|
||||
|
||||
Early development
|
||||
-------------------
|
||||
|
||||
* 0.1.0 - 0.5.0: Early development
|
||||
* tikz code from great.pres_manager
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
---
|
||||
format:
|
||||
pdf:
|
||||
include-in-header: prefobnicate.tex
|
||||
html:
|
||||
table-processing: false
|
||||
jupyter:
|
||||
keep-ipynb: true
|
||||
jupytext:
|
||||
formats: ipynb,qmd
|
||||
text_representation:
|
||||
extension: .qmd
|
||||
format_name: quarto
|
||||
format_version: '1.0'
|
||||
jupytext_version: 1.16.4
|
||||
kernelspec:
|
||||
display_name: Python 3 (ipykernel)
|
||||
language: python
|
||||
name: python3
|
||||
---
|
||||
|
||||
# Greater Tables
|
||||
|
||||
Creating presentation quality tables from pandas dataframes is frustrating. It is hard to left-align text and right-align numbers using pandas `display` or `df.to_html`. The `great_tables` package does a really nice job with pandas and polars dataframes but does not support indexes or TeX output.
|
||||
|
||||
This package provides consistent HTML and TeX table output with flexible type-based formatting, and table rules. Neither output relies on the pandas `to_html` or `to_latex` functions. TeX output uses Tikz tables for very tight control over layout and grid lines. The package is designed for use in Jupyter Lab notebooks Quarto documents.
|
||||
|
||||
Usage: the main class `GT` should be subclassed to set appropriate defaults for your project. `sGT` provides an example.
|
||||
|
||||
The project is currently in **beta** status. HTML output is better developed than TeX.
|
||||
|
||||
## The Name
|
||||
|
||||
Obviously, the name is a play on the `great_tables` package. But, I have been maintaining a set of macros called [GREATools](https://www.mynl.com/old/GREAT/home.html) (generalized, reusable, extensible actuarial tools) in VBA and Python since the late 1990s, and call all my macro packages "GREAT".
|
||||
|
||||
## Installation
|
||||
|
||||
```python
|
||||
pip install greater-tables
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The following example shows quite a hard table. It is formatted using the `sGT` class, which is a subclass of `GT` with a few defaults set.
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| output: asis
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from greater_tables import sGT
|
||||
level_1 = ["Group A", "Group A", "Group B", "Group B", 'Group C']
|
||||
level_2 = ['Sub 1', 'Sub 2', 'Sub 2', 'Sub 3', 'Sub 3']
|
||||
|
||||
multi_index = pd.MultiIndex.from_arrays([level_1, level_2])
|
||||
|
||||
start = pd.Timestamp.today().normalize() # Today's date, normalized to midnight
|
||||
end = pd.Timestamp(f"{start.year}-12-31") # End of the year
|
||||
|
||||
hard = pd.DataFrame(
|
||||
{'x': np.arange(2020, 2025, dtype=int),
|
||||
'a': np.array((100, 105, 2000, 2025, 100000), dtype=int),
|
||||
'b': 10. ** np.linspace(-9, 9, 5),
|
||||
'c': np.linspace(601, 4000, 5),
|
||||
'd': pd.date_range(start=start, end=end, periods=5),
|
||||
'e': 'once upon a time, risk is hard to define, not in Kansas anymore, neutrinos are hard to detect, $\\int_\\infty^\\infty e^{-x^2/2}dx$ is a hard integral'.split(',')
|
||||
}).set_index('x')
|
||||
hard.columns = multi_index
|
||||
sGT(hard, 'A hard table.')
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
The output illustrates:
|
||||
|
||||
* Quarto or Jupyter automatically the class's `_repr_html_` method (or `_repr_latex_` for pdf/TeX/Beamer output), providing seamless integration across different output formats.
|
||||
* Text is left-aligned, numbers are right-aligned.
|
||||
* The index is displayed, was detected as likely years, and formatted without a comma separator.
|
||||
* The first column of integers does have a comma thousands separator.
|
||||
* The second column of floats spans several orders of magnitude and is formatted using Engineering format, n for nano through G for giga.
|
||||
* The third column of floats is formatted with a comma separator and two decimals, based on the average absolute value.
|
||||
* The fourth column of date times is formatted as ISO standard dates (not date times).
|
||||
* The vertical lines separate the levels of the column multiindex. The subgroups are a little tricky.
|
||||
|
||||
More coming soon.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Available on [readthedocs](https://greater-tables-project.readthedocs.io/en/latest).
|
||||
|
||||
## Versions
|
||||
|
||||
### 1.1.1
|
||||
* Added logo, updated docs.
|
||||
|
||||
### 1.1.0
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
.. image:: https://img.shields.io/readthedocs/greater_tables_project
|
||||
:alt: Read the Docs
|
||||
|
||||
Release Notes
|
||||
===============
|
||||
|
||||
1.1.0
|
||||
------
|
||||
|
||||
* added ``formatters`` argument to pass in column specific formatters by name as a number (``n`` converts to ``{x:.nf}``, format string, or function
|
||||
* Added ```tabs`` argument to provide column widths
|
||||
* Added ``equal`` argument to provide hint that column widths should all be equal
|
||||
* Added ``caption_align='center'`` argument to set the caption alignment
|
||||
* Added ``large_ok=False`` argument, if ``False`` providing a dataframe with more than 100 rows throws an error. This function is expensive and is designed for small frames.
|
||||
|
||||
|
||||
1.0.0
|
||||
------
|
||||
|
||||
* Allow input via list of lists, or markdown table
|
||||
* Specify overall float format for whole table
|
||||
* Specify column alingment with 'llrc' style string
|
||||
* ``show_index`` option
|
||||
* Added more tests
|
||||
* Docs updated
|
||||
* Set tabs for width; use of width in HTML format.
|
||||
|
||||
|
||||
0.6.0
|
||||
------
|
||||
|
||||
* Initial release
|
||||
|
||||
Early development
|
||||
-------------------
|
||||
|
||||
* 0.1.0 - 0.5.0: Early development
|
||||
* tikz code from great.pres_manager
|
||||
|
||||
TODO
|
||||
=====
|
||||
|
||||
* Index aligners
|
||||
|
||||
|
||||
https://shields.io/badges/read-the-docs
|
||||
@@ -23,6 +23,7 @@ from rich import box
|
||||
from rich.table import Table
|
||||
|
||||
from . hasher import df_short_hash
|
||||
from . gtformats import GT_Format, TableFormat
|
||||
|
||||
# turn this fuck-fest off
|
||||
pd.set_option('future.no_silent_downcasting', True)
|
||||
@@ -61,43 +62,6 @@ class Breakability(IntEnum):
|
||||
ACCEPTABLE = 10
|
||||
|
||||
|
||||
# specify text mode
|
||||
Line = namedtuple('Line', ['begin', 'hline', 'sep', 'end', 'index_sep'])
|
||||
DataRow = namedtuple('DataRow', ['begin', 'sep', 'end', 'index_sep'])
|
||||
TableFormat = namedtuple('TableFormat', [
|
||||
'lineabove',
|
||||
'linebelowheader',
|
||||
'linebetweenrows',
|
||||
'linebelow',
|
||||
'headerrow',
|
||||
'datarow',
|
||||
'padding',
|
||||
'with_header_hide'
|
||||
])
|
||||
|
||||
# generic text format
|
||||
GT_Format = TableFormat(
|
||||
lineabove=Line('┍', '━', '┯', '┑', '┳'),
|
||||
linebelowheader=Line('┝', '━', '┿', '┥', '╋'),
|
||||
linebetweenrows=Line('├', '─', '┼', '┤', '╂'),
|
||||
linebelow=Line('┕', '━', '┷', '┙', '┻'),
|
||||
headerrow=DataRow('│', '│', '│', '┃'),
|
||||
datarow=DataRow('│', '│', '│', '┃'),
|
||||
padding=1,
|
||||
with_header_hide=None
|
||||
)
|
||||
|
||||
# GT_Format = TableFormat(
|
||||
# lineabove=Line('\u250d', '\u2501', '\u252f', '\u2511', '\u2533'),
|
||||
# linebelowheader=Line('\u251d', '\u2501', '\u253f', '\u2525', '\u254b'),
|
||||
# linebetweenrows=Line('\u251c', '\u2500', '\u253c', '\u2524', '\u2502'),
|
||||
# linebelow=Line('\u2515', '\u2501', '\u2537', '\u2519', '\u253b'),
|
||||
# headerrow=DataRow('\u2502', '\u2502', '\u2502', '\u2503'),
|
||||
# datarow=DataRow('\u2502', '\u2502', '\u2502', '\u2503'),
|
||||
# padding=1,
|
||||
# with_header_hide=None
|
||||
# )
|
||||
|
||||
|
||||
class GT(object):
|
||||
"""
|
||||
@@ -1716,7 +1680,10 @@ class GT(object):
|
||||
# dx = data in index
|
||||
# if this is the level that changes for this row
|
||||
# will use a top rule hence omit i = 0 which already has an hrule
|
||||
if i > 0 and hrule == '' and j == index_change_level[i]:
|
||||
# here have to be careful - if the index is not ! then not every row
|
||||
# appears in the index change level. But if it DOES NOT appear then
|
||||
# it isn't a change level so no rule required
|
||||
if i > 0 and hrule == '' and i in index_change_level and j == index_change_level[i]:
|
||||
hrule = f'grt-hrule-{j}'
|
||||
# html.append(f'<td class="grt-dx-r-{i} grt-dx-c-{j} {self.df_aligners[j]} {hrule}">{c}</td>')
|
||||
col_id = f'grt-c-{j}'
|
||||
|
||||
+80
-59
@@ -18,18 +18,21 @@ import sys
|
||||
from textwrap import wrap
|
||||
from typing import Optional, Union, Literal
|
||||
import warnings
|
||||
import yaml
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from cachetools import LRUCache
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.errors import IntCastingNaNError
|
||||
from pandas.api.types import is_datetime64_any_dtype, is_integer_dtype, \
|
||||
is_float_dtype # , is_numeric_dtype
|
||||
from pydantic import ValidationError
|
||||
from rich import box
|
||||
from rich.table import Table
|
||||
|
||||
from . gtenums import Breakability, Alignment
|
||||
from . gtformats import GT_Format, TableFormat
|
||||
from . gtformats import GT_Format, TableFormat, Line, DataRow
|
||||
from . gtconfig import GTConfigModel
|
||||
from . hasher import df_short_hash
|
||||
|
||||
@@ -242,7 +245,8 @@ class GT(object):
|
||||
**overrides,
|
||||
):
|
||||
if config and config_path:
|
||||
raise ValueError("Pass either 'config' or 'config_path', not both.")
|
||||
raise ValueError(
|
||||
"Pass either 'config' or 'config_path', not both.")
|
||||
|
||||
if config:
|
||||
base_config = config
|
||||
@@ -251,7 +255,8 @@ class GT(object):
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
base_config = GTConfigModel.model_validate(raw)
|
||||
except (ValidationError, OSError) as e:
|
||||
raise ValueError(f"Failed to load config from {config_path}") from e
|
||||
raise ValueError(f"Failed to load config from {
|
||||
config_path}") from e
|
||||
else:
|
||||
base_config = GTConfigModel()
|
||||
|
||||
@@ -402,7 +407,7 @@ class GT(object):
|
||||
self.raw_cols = raw_cols
|
||||
|
||||
# figure the default formatter (used in conjunction with raw columns)
|
||||
if config.default_formatter is None:
|
||||
if self.config.default_formatter is None:
|
||||
self.default_formatter = self.default_formatter
|
||||
else:
|
||||
assert callable(
|
||||
@@ -413,13 +418,13 @@ class GT(object):
|
||||
return config.default_formatter(x)
|
||||
except ValueError:
|
||||
return str(x)
|
||||
self.default_formatter = wrapped_config.default_formatter
|
||||
self.default_formatter = wrapped_default_formatter
|
||||
|
||||
# cast as much as possible to floats
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter(
|
||||
"ignore", category=pd.errors.PerformanceWarning)
|
||||
if config.cast_to_floats:
|
||||
if self.config.cast_to_floats:
|
||||
for i, c in enumerate(self.df.columns):
|
||||
if c in self.raw_cols or c in self.date_cols:
|
||||
continue
|
||||
@@ -552,10 +557,10 @@ class GT(object):
|
||||
# self.default_float_formatter = None
|
||||
# self.hrule_widths = hrule_widths or (0, 0, 0)
|
||||
# if not isinstance(self.config.hrule_widths, (list, tuple)):
|
||||
# self.config.hrule_widths = (self.config.hrule_widths,)
|
||||
# self.config.hrule_widths = (self.config.hrule_widths,)
|
||||
# self.vrule_widths = vrule_widths or (0, 0, 0)
|
||||
# if not isinstance(self.config.hrule_widths, (list, tuple)):
|
||||
# self.config.hrule_widths = (self.config.hrule_widths, )
|
||||
# self.config.hrule_widths = (self.config.hrule_widths, )
|
||||
# self.table_hrule_width = table_hrule_width
|
||||
# self.table_vrule_width = table_vrule_width
|
||||
# self.font_body = font_body
|
||||
@@ -570,28 +575,25 @@ class GT(object):
|
||||
elif isinstance(tabs, (int, float)):
|
||||
self.tabs = (tabs,)
|
||||
elif isinstance(tabs, (np.ndarray, list, tuple)):
|
||||
self.tabs = tabs # Already iterable, self.config.tabs = as is
|
||||
self.tabs = tabs # Already iterable, self.tabs = as is
|
||||
else:
|
||||
self.tabs = [tabs] # Fallback for anything else
|
||||
# self.equal = equal
|
||||
|
||||
if config.padding_trbl is None:
|
||||
if config.spacing == 'tight':
|
||||
config.padding_trbl = (0, 5, 0, 5)
|
||||
elif config.spacing == 'medium':
|
||||
config.padding_trbl = (2, 10, 2, 10)
|
||||
elif config.spacing == 'wide':
|
||||
config.padding_trbl = (4, 15, 4, 15)
|
||||
if self.config.padding_trbl is not None:
|
||||
padding_trbl = self.config_padding_trbl
|
||||
elif self.config.padding_trbl is None:
|
||||
if self.config.spacing == 'tight':
|
||||
padding_trbl = (0, 5, 0, 5)
|
||||
elif self.config.spacing == 'medium':
|
||||
padding_trbl = (2, 10, 2, 10)
|
||||
elif self.config.spacing == 'wide':
|
||||
padding_trbl = (4, 15, 4, 15)
|
||||
else:
|
||||
raise ValueError(
|
||||
'config.spacing must be tight, medium, or wide or tuple of four ints.')
|
||||
try:
|
||||
self.padt, self.padr, self.padb, self.padl = config.padding_trbl
|
||||
except ValueError:
|
||||
# pydantics will see to this...
|
||||
logger.error(
|
||||
f'config.padding_trbl {config.padding_trbl=}, must be four ints, defaulting to medium padding')
|
||||
self.padt, self.padr, self.padb, self.padl = 2, 10, 2, 10
|
||||
# pydantic will see to it this is OK
|
||||
self.padt, self.padr, self.padb, self.padl = padding_trbl
|
||||
|
||||
# because of the problem of non-unique indexes use a list and
|
||||
# not a dict to pass the formatters to to_html
|
||||
@@ -608,7 +610,7 @@ class GT(object):
|
||||
# cache for various things...
|
||||
self._cache = LRUCache(20)
|
||||
# config.sparsify
|
||||
if config.sparsify and self.nindex > 1:
|
||||
if self.config.sparsify and self.nindex > 1:
|
||||
self.df = GT.sparsify(self.df, self.df.columns[:self.nindex])
|
||||
# for c in self.df.columns[:self.nindex]:
|
||||
# # config.sparsify returns some other stuff...
|
||||
@@ -857,7 +859,7 @@ class GT(object):
|
||||
self.default_float_formatter or self.make_float_formatter(self.df.iloc[:, i]))
|
||||
else:
|
||||
# print(f'{i} default')
|
||||
self._df_formatters.append(self.config.default_formatter)
|
||||
self._df_formatters.append(self.default_formatter)
|
||||
# self._df_formatters is now a list of length config.equal to cols in df
|
||||
if len(self._df_formatters) != self.df.shape[1]:
|
||||
raise ValueError(
|
||||
@@ -925,8 +927,10 @@ class GT(object):
|
||||
PADDING = 2 # per column
|
||||
if self.config.table_width_mode == 'explicit':
|
||||
# target width INCLUDES padding and column marks |
|
||||
target_width = self.config.max_table_width - (PADDING + 1) * n_col - 1
|
||||
logger.info(f'Col padding effect {self.config.max_table_width=} ==> {target_width=}')
|
||||
target_width = self.config.max_table_width - \
|
||||
(PADDING + 1) * n_col - 1
|
||||
logger.info(f'Col padding effect {
|
||||
self.config.max_table_width=} ==> {target_width=}')
|
||||
elif self.config.table_width_mode == 'natural':
|
||||
target_width = natural + (PADDING + 1) * n_col + 1
|
||||
elif self.config.table_width_mode == 'breakable':
|
||||
@@ -936,7 +940,8 @@ class GT(object):
|
||||
|
||||
# extra space for the headers to relax, if useful
|
||||
if self.config.table_width_header_adjust > 0:
|
||||
max_extra = int(self.config.table_width_header_adjust * target_width)
|
||||
max_extra = int(
|
||||
self.config.table_width_header_adjust * target_width)
|
||||
else:
|
||||
max_extra = 0
|
||||
|
||||
@@ -1004,7 +1009,14 @@ class GT(object):
|
||||
ans['recommended'], ans['natural_w_header'])
|
||||
|
||||
# Ensure final constraint
|
||||
ans['recommended'] = ans['recommended'].astype(int)
|
||||
try:
|
||||
ans['recommended'] = ans['recommended'].astype(int)
|
||||
except IntCastingNaNError:
|
||||
print('getting error')
|
||||
print(ans['recommended'])
|
||||
ans['recommended'] = pd.to_numeric(
|
||||
ans['recommended'], errors='coerce').fillna(0).astype(int)
|
||||
|
||||
logger.info("Raw rec: %s\tTweaks: %s\tActual: %s\tTarget: %s\tOver/(U): %s",
|
||||
ans['raw_rec'].sum(),
|
||||
ans['header_tweak'].sum(),
|
||||
@@ -1517,7 +1529,7 @@ class GT(object):
|
||||
font-weight: bold;
|
||||
}}
|
||||
''']
|
||||
for i, w in enumerate(config.tabs):
|
||||
for i, w in enumerate(tabs):
|
||||
style.append(f' #{self.df_id} .grt-c-{i} {{ width: {w}em; }}')
|
||||
style.append('</style>')
|
||||
logger.info('CREATED CSS')
|
||||
@@ -1547,15 +1559,15 @@ class GT(object):
|
||||
colw, tabs = GT.estimate_column_widths(
|
||||
self.df, self.config.max_table_width, nc_index=self.nindex, scale=1, equal=self.config.equal)
|
||||
if self.config.debug:
|
||||
print(f'Make html Input {self.config.tabs=}\nComputed {tabs=}')
|
||||
if self.config.tabs is not None:
|
||||
if len(tabs) == len(self.config.tabs):
|
||||
tabs = self.config.tabs
|
||||
elif len(self.config.tabs) == 1:
|
||||
tabs = self.config.tabs * len(tabs)
|
||||
print(f'Make html Input {self.tabs=}\nComputed {tabs=}')
|
||||
if self.tabs is not None:
|
||||
if len(tabs) == len(self.tabs):
|
||||
tabs = self.tabs
|
||||
elif len(self.tabs) == 1:
|
||||
tabs = self.tabs * len(tabs)
|
||||
else:
|
||||
logger.error(
|
||||
f'{self.config.tabs=} must be None, a single number, or a list of numbers of the correct length. Ignoring.')
|
||||
f'{self.tabs=} must be None, a single number, or a list of numbers of the correct length. Ignoring.')
|
||||
# print('HTML ' + ', '.join([f'{c:,.2f}' for c in tabs]))
|
||||
|
||||
# set column widths; tabs returns lengths of strings in each column
|
||||
@@ -1665,7 +1677,9 @@ class GT(object):
|
||||
# dx = data in index
|
||||
# if this is the level that changes for this row
|
||||
# will use a top rule hence omit i = 0 which already has an hrule
|
||||
if i > 0 and hrule == '' and j == index_change_level[i]:
|
||||
# appears in the index change level. But if it DOES NOT appear then
|
||||
# it isn't a change level so no rule required
|
||||
if i > 0 and hrule == '' and i in index_change_level and j == index_change_level[i]:
|
||||
hrule = f'grt-hrule-{j}'
|
||||
# html.append(f'<td class="grt-dx-r-{i} grt-dx-c-{j} {self.df_aligners[j]} {hrule}">{c}</td>')
|
||||
col_id = f'grt-c-{j}'
|
||||
@@ -1752,8 +1766,12 @@ class GT(object):
|
||||
@staticmethod
|
||||
def apply_formatters_work(df, formatters):
|
||||
"""Apply formatters to a DataFrame."""
|
||||
new_df = pd.DataFrame({i: map(f, df.iloc[:, i])
|
||||
for i, f in enumerate(formatters)})
|
||||
try:
|
||||
new_df = pd.DataFrame({i: map(f, df.iloc[:, i])
|
||||
for i, f in enumerate(formatters)})
|
||||
except TypeError:
|
||||
print('NASTY TYPE ERROR')
|
||||
raise
|
||||
new_df.columns = df.columns
|
||||
return new_df
|
||||
|
||||
@@ -1899,7 +1917,7 @@ class GT(object):
|
||||
row sep={row_sep}em,
|
||||
column sep={column_sep}em,
|
||||
nodes in empty cells,
|
||||
nodes={{rectangle, scale={scale}, text badly ragged {config.debug}}},
|
||||
nodes={{rectangle, scale={scale}, text badly ragged {debug}}},
|
||||
"""
|
||||
# put draw=blue!10 or so in nodes to see the node
|
||||
|
||||
@@ -1928,12 +1946,12 @@ class GT(object):
|
||||
"ignore", category=pd.errors.PerformanceWarning)
|
||||
df = df.reset_index(
|
||||
drop=False, col_level=df.columns.nlevels - 1)
|
||||
if config.sparsify:
|
||||
if sparsify:
|
||||
if hrule is None:
|
||||
hrule = set()
|
||||
for i in range(config.sparsify):
|
||||
# TODO update to new config.sparsify!!
|
||||
df.iloc[:, i], rules = GT.config.sparsify_old(df.iloc[:, i])
|
||||
for i in range(sparsify):
|
||||
# TODO update to new sparsify!!
|
||||
df.iloc[:, i], rules = GT.sparsify_old(df.iloc[:, i])
|
||||
# don't want lines everywhere
|
||||
if len(rules) < len(df) - 1:
|
||||
hrule = set(hrule).union(rules)
|
||||
@@ -1960,15 +1978,15 @@ class GT(object):
|
||||
# estimate... originally called guess_column_widths, with more parameters
|
||||
colw, tabs = GT.estimate_column_widths(df, self.config.max_table_width, nc_index=nc_index, scale=self.config.tikz_scale, equal=self.config.equal) # noqa
|
||||
if self.config.debug:
|
||||
print(f'Make TikZ Input {self.config.tabs=}\nComputed {tabs=}')
|
||||
if self.config.tabs is not None:
|
||||
if len(tabs) == len(self.config.tabs):
|
||||
tabs = self.config.tabs
|
||||
elif len(self.config.tabs) == 1:
|
||||
tabs = self.config.tabs * len(tabs)
|
||||
print(f'Make TikZ Input {self.tabs=}\nComputed {tabs=}')
|
||||
if self.tabs is not None:
|
||||
if len(tabs) == len(self.tabs):
|
||||
tabs = self.tabs
|
||||
elif len(self.tabs) == 1:
|
||||
tabs = self.tabs * len(tabs)
|
||||
else:
|
||||
logger.error(
|
||||
f'{self.config.tabs=} must be None, a single number, or a list of numbers of the correct length. Ignoring.')
|
||||
f'{self.tabs=} must be None, a single number, or a list of numbers of the correct length. Ignoring.')
|
||||
# print('TIKZ ' + ', '.join([f'{c:,.2f}' for c in tabs]))
|
||||
# print(f'TIKZ {colw=}, {tabs=}')
|
||||
logger.info(f'tabs: {tabs}')
|
||||
@@ -1995,10 +2013,12 @@ class GT(object):
|
||||
latex = ''
|
||||
else:
|
||||
latex = f'[{latex}]'
|
||||
config.debug = ''
|
||||
debug = ''
|
||||
if self.config.debug:
|
||||
# color all boxes
|
||||
config.debug = ', draw=blue!10'
|
||||
debug = ', draw=blue!10'
|
||||
else:
|
||||
debug = ''
|
||||
sio.write(header.format(container_env=container_env,
|
||||
caption=caption,
|
||||
extra_defs=extra_defs,
|
||||
@@ -2006,7 +2026,7 @@ class GT(object):
|
||||
column_sep=column_sep,
|
||||
row_sep=row_sep,
|
||||
latex=latex,
|
||||
debug=self.config.debug))
|
||||
debug=debug))
|
||||
|
||||
# table header
|
||||
# title rows, start with the empty spacer row
|
||||
@@ -2024,7 +2044,8 @@ class GT(object):
|
||||
if i == 1:
|
||||
# first column sets row height for entire row
|
||||
sio.write(f'\tcolumn {i:>2d}/.style={{'
|
||||
f'nodes={{align={ad[al]:<6s}}}, text height=0.9em, text depth=0.2em, '
|
||||
f'nodes={{align={
|
||||
ad[al]:<6s}}}, text height=0.9em, text depth=0.2em, '
|
||||
f'inner xsep={column_sep}em, inner ysep=0, '
|
||||
f'text width={max(2, 0.6 * w):.2f}em}},\n')
|
||||
else:
|
||||
@@ -2053,7 +2074,7 @@ class GT(object):
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
for lvl in range(len(df.columns.levels)):
|
||||
nl = ''
|
||||
sparse_columns[lvl], mi_vrules[lvl] = GT.config.sparsify_mi(df.columns.get_level_values(lvl),
|
||||
sparse_columns[lvl], mi_vrules[lvl] = GT.sparsify_mi(df.columns.get_level_values(lvl),
|
||||
lvl == len(df.columns.levels) - 1)
|
||||
for cn, c, al in zip(df.columns, sparse_columns[lvl], align):
|
||||
# c = wfloat_format(c)
|
||||
@@ -2268,7 +2289,7 @@ class GT(object):
|
||||
# data all seems about the same width
|
||||
tabs.append(common_size)
|
||||
logger.info(f'Determined tab config.spacing: {tabs}')
|
||||
if config.equal:
|
||||
if equal:
|
||||
# see if config.equal widths makes sense
|
||||
dt = tabs[nl:]
|
||||
if max(dt) / sum(dt) < 4 / 3:
|
||||
@@ -2302,7 +2323,7 @@ class GT(object):
|
||||
@staticmethod
|
||||
def sparsify_old(col):
|
||||
"""
|
||||
config.sparsify col values, col a pd.Series or dict, with items and accessor
|
||||
sparsify col values, col a pd.Series or dict, with items and accessor
|
||||
column results from a reset_index so has index 0,1,2... this is relied upon.
|
||||
TODO: this doesn't work if there is a change in a higher level but not this level
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Define text table formats.
|
||||
|
||||
@@ -80,3 +81,11 @@ GT_Format = TableFormat(
|
||||
# padding=1,
|
||||
# with_header_hide=None
|
||||
# )
|
||||
|
||||
|
||||
def default_formatter(x):
|
||||
"""
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
+287
-246
@@ -4,262 +4,226 @@ Make fake dataframes for testing.
|
||||
GPT from SJMM design.
|
||||
"""
|
||||
|
||||
# from pathlib import Path
|
||||
# from dataclasses import dataclass, field
|
||||
# from typing import Optional, Union
|
||||
# from datetime import datetime, timedelta
|
||||
# import hashlib
|
||||
# import re
|
||||
|
||||
|
||||
# import numpy as np
|
||||
# import pandas as pd
|
||||
# from faker import Faker
|
||||
|
||||
|
||||
# @dataclass
|
||||
# class TestDataFrameFactory:
|
||||
# """
|
||||
# Factory for generating small synthetic pandas DataFrames for testing.
|
||||
|
||||
# Attributes:
|
||||
# colname_words: Optional list of strings to use for column names.
|
||||
# default_word_count: Max number of words for string columns (default 3).
|
||||
# seed: Optional random seed. If None, one is generated.
|
||||
# """
|
||||
# colname_words: Optional[list[str]] = None
|
||||
# default_word_count: int = 3
|
||||
# seed: Optional[int] = None
|
||||
# _last_args: dict = field(default_factory=dict, init=False)
|
||||
|
||||
# def __post_init__(self):
|
||||
# self.faker = Faker()
|
||||
# self.seed = int(self.seed if self.seed is not None else np.random.SeedSequence().entropy)
|
||||
# self.rng = np.random.default_rng(self.seed)
|
||||
|
||||
# def make(self, rows: int, columns: Union[int, str], index: Union[int, str] = 0,
|
||||
# col_index: Union[int, str] = 0, missing: float = 0.0) -> pd.DataFrame:
|
||||
# """
|
||||
# Generate a test DataFrame with the given specification.
|
||||
|
||||
# Args:
|
||||
# rows: Number of rows.
|
||||
# columns: Column type spec (int for all float cols, or string type codes).
|
||||
# index: Index level types (int for RangeIndex or string like 'ti').
|
||||
# col_index: Column index levels (same format as `index`).
|
||||
# missing: Proportion of missing data in each column.
|
||||
|
||||
# Returns:
|
||||
# DataFrame
|
||||
# """
|
||||
# self._last_args = dict(rows=rows, columns=columns, index=index, col_index=col_index, missing=missing)
|
||||
# return self._generate(**self._last_args)
|
||||
|
||||
# def another(self, new_seed: bool = True) -> pd.DataFrame:
|
||||
# """
|
||||
# Generate another DataFrame with the last parameters.
|
||||
|
||||
# Args:
|
||||
# new_seed: If True, re-randomize the generator seed.
|
||||
|
||||
# Returns:
|
||||
# DataFrame
|
||||
# """
|
||||
# if new_seed:
|
||||
# self.seed = int(np.random.SeedSequence().entropy)
|
||||
# self.rng = np.random.default_rng(self.seed)
|
||||
# return self._generate(**self._last_args)
|
||||
|
||||
# def random(self, index_levels: int = 1, column_levels: int = 1) -> pd.DataFrame:
|
||||
# """
|
||||
# Generate a DataFrame with randomly chosen settings.
|
||||
|
||||
# Args:
|
||||
# index_levels: Number of index levels to use.
|
||||
# column_levels: Number of column MultiIndex levels.
|
||||
|
||||
# Returns:
|
||||
# DataFrame
|
||||
# """
|
||||
# rows = self.rng.integers(10, 50)
|
||||
# col_types = self.rng.choice(['d', 'f', 'i', 's1', 's3', 's7', 'h', 't', 'p'], size=self.rng.integers(3, 7))
|
||||
# missing = round(float(self.rng.uniform(0, 0.15)), 2)
|
||||
# index = ''.join(self.rng.choice(['t', 'd', 'i', 's2'], size=index_levels))
|
||||
# col_index = ''.join(self.rng.choice(['s', 'i', 'd'], size=column_levels))
|
||||
# return self.make(rows=rows, columns=''.join(col_types), index=index, col_index=col_index, missing=missing)
|
||||
|
||||
# def _parse_colspec(self, spec: str) -> list[str]:
|
||||
# return re.findall(r's\d+|[a-z]', spec)
|
||||
|
||||
|
||||
# def _generate(self, rows: int, columns: Union[int, str], index: Union[int, str],
|
||||
# col_index: Union[int, str], missing: float) -> pd.DataFrame:
|
||||
# if isinstance(columns, int):
|
||||
# col_types = ['s3'] * columns
|
||||
# else:
|
||||
# col_types = self._parse_colspec(columns)
|
||||
|
||||
# colnames = self._make_column_names(len(col_types))
|
||||
# data = {
|
||||
# name: self._generate_column(dt, rows) for name, dt in zip(colnames, col_types)
|
||||
# }
|
||||
# df = pd.DataFrame(data)
|
||||
# df.index = self._make_index(index, rows, "i")
|
||||
# df.columns = self._make_index(col_index, len(df.columns), "c") if isinstance(col_index, str) else df.columns
|
||||
# df = self._insert_missing(df, missing)
|
||||
# return df
|
||||
|
||||
# def _make_column_names(self, n: int) -> list[str]:
|
||||
# if self.colname_words:
|
||||
# pool = self.colname_words
|
||||
# else:
|
||||
# pool = [self.faker.word() for _ in range(n * 2)]
|
||||
# names = []
|
||||
# used = set()
|
||||
# for word in pool:
|
||||
# if len(names) >= n:
|
||||
# break
|
||||
# if word not in used:
|
||||
# names.append(word)
|
||||
# used.add(word)
|
||||
# while len(names) < n:
|
||||
# names.append(f"col_{len(names)}")
|
||||
# return names
|
||||
|
||||
# def _generate_column(self, dtype: str, n: int) -> pd.Series:
|
||||
# if dtype.startswith('s'):
|
||||
# max_words = int(dtype[1:]) if len(dtype) > 1 else self.default_word_count
|
||||
# return pd.Series([" ".join(self.faker.words(self.rng.integers(max_words // 2 + 1, max_words + 1))) for _ in range(n)])
|
||||
# if dtype == 'f':
|
||||
# return pd.Series(self.rng.normal(loc=100, scale=25, size=n))
|
||||
# if dtype == 'i':
|
||||
# return pd.Series(self.rng.integers(1e9, 1e12, size=n), dtype='int64')
|
||||
# if dtype == 'd':
|
||||
# start_date = self.faker.date_between(start_date='-10y', end_date='today')
|
||||
# return pd.Series(pd.date_range(start=start_date, periods=n, freq='D'))
|
||||
# if dtype == 't':
|
||||
# start_dt = datetime.now() - timedelta(days=365 * 2)
|
||||
# return pd.Series([start_dt + timedelta(minutes=int(self.rng.integers(0, 2 * 365 * 24 * 60))) for _ in range(n)])
|
||||
# if dtype == 'h':
|
||||
# return pd.Series([
|
||||
# hashlib.blake2b(f"val{i}".encode(), digest_size=32).hexdigest()
|
||||
# for i in range(n)
|
||||
# ])
|
||||
# if dtype == 'p':
|
||||
# return pd.Series([str(Path(f"/data/{self.faker.word()}/{i}.dat")) for i in range(n)])
|
||||
# raise ValueError(f"Unknown dtype: {dtype}")
|
||||
|
||||
# def _make_index(self, desc: Union[int, str], n: int, label_prefix: str) -> pd.Index:
|
||||
# if isinstance(desc, int):
|
||||
# return pd.RangeIndex(n, name=f"{label_prefix}0")
|
||||
# levels = []
|
||||
# names = []
|
||||
# for j, dt in enumerate(desc):
|
||||
# s = self._generate_column(dt, n)
|
||||
# levels.append(s)
|
||||
# names.append(f"{label_prefix}{j}")
|
||||
# return pd.MultiIndex.from_arrays(levels, names=names)
|
||||
|
||||
# def _insert_missing(self, df: pd.DataFrame, prop: float) -> pd.DataFrame:
|
||||
# if prop <= 0:
|
||||
# return df
|
||||
# n_rows = df.shape[0]
|
||||
# for col in df.columns:
|
||||
# n_missing = max(1, int(np.floor(prop * n_rows)))
|
||||
# missing_indices = self.rng.choice(n_rows, size=n_missing, replace=False)
|
||||
# df.iloc[missing_indices, df.columns.get_loc(col)] = np.nan
|
||||
# return df
|
||||
|
||||
|
||||
# Reimport necessary modules after kernel reset
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Union
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import cycle
|
||||
from math import prod
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
import hashlib
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from faker import Faker
|
||||
import random
|
||||
import re
|
||||
|
||||
@dataclass
|
||||
class TestDataFrameFactory:
|
||||
colname_words: Optional[list[str]] = None
|
||||
default_word_count: int = 3
|
||||
seed: Optional[int] = None
|
||||
_last_args: dict = field(default_factory=dict, init=False)
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
def __post_init__(self):
|
||||
self.faker = Faker()
|
||||
self.seed = int(self.seed if self.seed is not None else np.random.SeedSequence().entropy)
|
||||
|
||||
name_word_list = [
|
||||
"account",
|
||||
"address",
|
||||
"amount",
|
||||
"balance",
|
||||
"category",
|
||||
"client",
|
||||
"combined ratio",
|
||||
"comment",
|
||||
"currency",
|
||||
"description",
|
||||
"duration",
|
||||
"email",
|
||||
"entry",
|
||||
"estimate",
|
||||
"extension",
|
||||
"failure",
|
||||
"filename",
|
||||
"identifier",
|
||||
"location",
|
||||
"loss ratio",
|
||||
"note",
|
||||
"operation",
|
||||
"premium",
|
||||
"processing",
|
||||
"project",
|
||||
"reference",
|
||||
"remark",
|
||||
"status",
|
||||
"supplier",
|
||||
"timestamp",
|
||||
"transaction",
|
||||
"type",
|
||||
"user",
|
||||
'expense ratio',
|
||||
'loss date'
|
||||
]
|
||||
|
||||
|
||||
class TestDataFrameFactory:
|
||||
"""
|
||||
Create super-dooper test dataframes.
|
||||
"""
|
||||
|
||||
def __init__(self, seed: Optional[int] = None):
|
||||
"""
|
||||
Factory for generating small synthetic pandas DataFrames for testing.
|
||||
|
||||
Attributes:
|
||||
seed: Optional random seed. If None, one is generated.
|
||||
"""
|
||||
self._last_args = {}
|
||||
self.seed = int(
|
||||
seed if seed is not None else np.random.SeedSequence().entropy)
|
||||
|
||||
# rng
|
||||
self.rng = np.random.default_rng(self.seed)
|
||||
|
||||
# word list for names of index levels
|
||||
nwl = name_word_list[:]
|
||||
random.shuffle(nwl)
|
||||
self._index_namer = cycle(nwl)
|
||||
|
||||
# read words and create cycler
|
||||
p = Path(__file__).parent / 'words-12.md'
|
||||
assert p.exists()
|
||||
txt = p.read_text(encoding='utf-8')
|
||||
word_list = txt.split('\n')
|
||||
temp = word_list[:]
|
||||
random.shuffle(temp)
|
||||
self._word_gen = cycle(temp)
|
||||
|
||||
# read tex expressions and create cycler
|
||||
tex_list = pd.read_csv(Path(__file__).parent /
|
||||
'tex_list.csv')['expr'].to_list()
|
||||
tex_list = [i for i in tex_list if len(i) < 50]
|
||||
random.shuffle(tex_list)
|
||||
self._tex_gen = cycle(tex_list)
|
||||
|
||||
# lengths of index (word count) sampled from:
|
||||
self.index_value_lengths = [1]*10 + [2] * 4 + [3]
|
||||
|
||||
def make(self, rows: int, columns: Union[int, str], index: Union[int, str] = 0,
|
||||
col_index: Union[int, str] = 0, missing: float = 0.0) -> pd.DataFrame:
|
||||
self._last_args = dict(rows=rows, columns=columns, index=index, col_index=col_index, missing=missing)
|
||||
return self._generate(**self._last_args).sort_index()
|
||||
"""
|
||||
Generate a test DataFrame with the given specification.
|
||||
|
||||
Data types
|
||||
|
||||
d date
|
||||
f float
|
||||
h hash
|
||||
i integer
|
||||
l log float (greater range than float)
|
||||
m year - month
|
||||
p path (filename)
|
||||
sx string length x
|
||||
t time
|
||||
x tex text - an equation
|
||||
y year
|
||||
|
||||
|
||||
Args:
|
||||
rows: Number of rows.
|
||||
columns: Column type spec (int for all float cols, or string type codes).
|
||||
index: Index level types (int for RangeIndex or string like 'ti').
|
||||
col_index: Column index levels (same format as `index`).
|
||||
missing: Proportion of missing data in each column.
|
||||
|
||||
Returns:
|
||||
DataFrame
|
||||
"""
|
||||
self._last_args = dict(rows=rows, columns=columns,
|
||||
index=index, col_index=col_index, missing=missing)
|
||||
return self._generate(**self._last_args)
|
||||
|
||||
def another(self, new_seed: bool = True) -> pd.DataFrame:
|
||||
"""
|
||||
Generate another DataFrame with the last parameters.
|
||||
|
||||
Args:
|
||||
new_seed: If True, re-randomize the generator seed.
|
||||
|
||||
Returns:
|
||||
DataFrame
|
||||
"""
|
||||
if new_seed:
|
||||
self.seed = int(np.random.SeedSequence().entropy)
|
||||
self.rng = np.random.default_rng(self.seed)
|
||||
return self._generate(**self._last_args).sort_index()
|
||||
return self._generate(**self._last_args)
|
||||
|
||||
def random(self, index_levels: int = 1, column_levels: int = 1) -> pd.DataFrame:
|
||||
rows = self.rng.integers(10, 50)
|
||||
col_types = self.rng.choice(['d', 'f', 'i', 's3', 'h', 't', 'p'], size=self.rng.integers(3, 7))
|
||||
def random(self, index_levels: int = 0, column_levels: int = 0) -> pd.DataFrame:
|
||||
"""
|
||||
Generate a DataFrame with randomly chosen settings.
|
||||
|
||||
Args:
|
||||
index_levels: Number of index levels to use.
|
||||
column_levels: Number of column MultiIndex levels.
|
||||
|
||||
Returns:
|
||||
DataFrame
|
||||
"""
|
||||
if index_levels == 0:
|
||||
index_levels = random.choice([1, 1, 1, 1, 1, 2, 2, 3])
|
||||
if column_levels == 0:
|
||||
column_levels = random.choice([1, 1, 1, 1, 1, 2, 2, 3])
|
||||
rows = self.rng.integers(5 * index_levels, 10 * index_levels)
|
||||
col_types = self.rng.choice(
|
||||
['d', 'f', 'i', 's3', 'l', 'h', 't', 'p'], size=self.rng.integers(3, 7))
|
||||
missing = round(float(self.rng.uniform(0, 0.15)), 2)
|
||||
index = ''.join(self.rng.choice(['t', 'd', 'i', 's2'], size=index_levels))
|
||||
col_index = ''.join(self.rng.choice(['s', 'i', 'd'], size=column_levels))
|
||||
index = ''.join(self.rng.choice(
|
||||
['t', 'd', 'i', 's2'], size=index_levels))
|
||||
col_index = ''.join(self.rng.choice(
|
||||
['s', 's2', 's2', 's3'], size=column_levels))
|
||||
return self.make(rows=rows, columns=''.join(col_types), index=index, col_index=col_index, missing=missing)
|
||||
|
||||
def _generate(self, rows: int, columns: Union[int, str], index: Union[int, str],
|
||||
col_index: Union[int, str], missing: float) -> pd.DataFrame:
|
||||
col_types = ['f'] * columns if isinstance(columns, int) else self._parse_colspec(columns)
|
||||
colnames = self._make_column_names(len(col_types))
|
||||
data = {
|
||||
name: self._generate_column(dt, rows) for name, dt in zip(colnames, col_types)
|
||||
}
|
||||
df = pd.DataFrame(data)
|
||||
df.index = self._make_index(index, rows, "i")
|
||||
df.columns = self._make_index(col_index, len(df.columns), "c") if isinstance(col_index, str) else df.columns
|
||||
# if columns is an int then make up types
|
||||
if isinstance(columns, int):
|
||||
col_types = self.rng.choice(
|
||||
['d', 't', 'f', 'l', 'i', 's1', 's3', 's9', 'h', 'p', 'x'], size=columns)
|
||||
else:
|
||||
col_types = self._parse_colspec(columns)
|
||||
# if col_index is an int then use all strings of that depth
|
||||
if isinstance(col_index, int):
|
||||
col_index_types = ['s'] * col_index
|
||||
else:
|
||||
col_index_types = self._parse_colspec(col_index)
|
||||
if isinstance(index, int):
|
||||
index = ['s'] * index
|
||||
else:
|
||||
index = self._parse_colspec(index)
|
||||
print(index)
|
||||
# col names are a transposed index.
|
||||
df = pd.DataFrame(index=range(rows))
|
||||
col_idx = self._make_index(col_index_types, len(col_types))
|
||||
for dt, c in zip(col_types, range(len(col_idx))):
|
||||
df[c] = self._generate_column(dt, rows)
|
||||
df.columns = col_idx
|
||||
df.index = self._make_index(index, rows)
|
||||
df = self._insert_missing(df, missing)
|
||||
return df
|
||||
|
||||
def _parse_colspec(self, spec: str) -> list[str]:
|
||||
return re.findall(r's\d+|[a-z]', spec)
|
||||
|
||||
def _make_column_names(self, n: int) -> list[str]:
|
||||
if self.colname_words:
|
||||
pool = self.colname_words
|
||||
else:
|
||||
pool = [self.faker.word() for _ in range(n * 2)]
|
||||
names, used = [], set()
|
||||
for word in pool:
|
||||
if len(names) >= n:
|
||||
break
|
||||
if word not in used:
|
||||
names.append(word)
|
||||
used.add(word)
|
||||
while len(names) < n:
|
||||
names.append(f"col_{len(names)}")
|
||||
return names
|
||||
|
||||
def _generate_column(self, dtype: str, n: int) -> pd.Series:
|
||||
if dtype.startswith('s'):
|
||||
max_words = int(dtype[1:]) if len(dtype) > 1 else self.default_word_count
|
||||
return pd.Series([" ".join(self.faker.words(self.rng.integers(1, max_words + 1))) for _ in range(n)])
|
||||
max_words = int(dtype[1:]) if len(dtype) > 1 else 1
|
||||
return pd.Series([" ".join(self.word() for i in range(max_words)) for j in range(n)])
|
||||
if dtype == 'f':
|
||||
return pd.Series(self.rng.normal(loc=100, scale=25, size=n))
|
||||
return pd.Series(self.rng.normal(loc=100000, scale=250000, size=n))
|
||||
if dtype == 'l':
|
||||
# log float (greater range)
|
||||
return pd.Series(np.exp(self.rng.normal(loc=-4 / 2 + 4, scale=4, size=n)))
|
||||
if dtype == 'i':
|
||||
return pd.Series(self.rng.integers(1e9, 1e12, size=n), dtype='int64')
|
||||
return pd.Series(self.rng.integers(-1e4, 1e6, size=n), dtype='int64')
|
||||
if dtype == 'd':
|
||||
start_date = self.faker.date_between(start_date='-10y', end_date='today')
|
||||
start_date = TestDataFrameFactory.random_date_within_last_n_years(
|
||||
10)
|
||||
return pd.Series(pd.date_range(start=start_date, periods=n, freq='D'))
|
||||
if dtype == 't':
|
||||
start_dt = datetime.now() - timedelta(days=365 * 2)
|
||||
return pd.Series([
|
||||
start_dt + timedelta(minutes=int(self.rng.integers(0, 2 * 365 * 24 * 60)))
|
||||
start_dt +
|
||||
timedelta(minutes=int(self.rng.integers(0, 2 * 365 * 24 * 60)))
|
||||
for _ in range(n)
|
||||
])
|
||||
if dtype == 'h':
|
||||
@@ -268,45 +232,122 @@ class TestDataFrameFactory:
|
||||
for i in range(n)
|
||||
])
|
||||
if dtype == 'p':
|
||||
return pd.Series([str(Path(f"/data/{self.faker.word()}/{i}.dat")) for i in range(n)])
|
||||
return pd.Series([str(Path(f"/data/{self.word()}/{i}.dat")) for i in range(n)])
|
||||
if dtype == 'x':
|
||||
# tex
|
||||
return pd.Series([self.tex() for i in range(n)])
|
||||
raise ValueError(f"Unknown dtype: {dtype}")
|
||||
|
||||
def _make_index(self, desc: Union[int, str], n: int, label_prefix: str) -> pd.Index:
|
||||
def _make_index(self, desc: Union[int, str, list[str]], n: int) -> pd.Index:
|
||||
if isinstance(desc, int):
|
||||
return pd.RangeIndex(n, name=f"{label_prefix}0")
|
||||
return pd.RangeIndex(n, name=self.index_name())
|
||||
if isinstance(desc, str):
|
||||
desc = self._parse_colspec(desc)
|
||||
if len(desc) == 1:
|
||||
s = self._generate_column(desc[0], n)
|
||||
return pd.Index(s, name=f"{label_prefix}0")
|
||||
return self._make_hierarchical_index(desc, n, label_prefix)
|
||||
if desc[0] == 'i':
|
||||
return pd.RangeIndex(n, name=self.index_name())
|
||||
elif desc[0] in ('d', 't', 'x'):
|
||||
vals = self._generate_column(desc[0], n)
|
||||
return pd.Index(vals, name=self.index_name())
|
||||
elif not all(i[0] == 's' for i in desc):
|
||||
raise ValueError(
|
||||
f'Inadmissible index spec: only string, int, and date types allowed, not {desc}.')
|
||||
level_value_lengths = [1 if len(i) == 1 else int(i[1:]) for i in desc]
|
||||
return self.make_index(rows=n, levels=len(desc), level_value_lengths=level_value_lengths,
|
||||
p0=1, padding=2)
|
||||
|
||||
def _make_hierarchical_index(self, desc: str, n: int, label_prefix: str) -> pd.MultiIndex:
|
||||
"""
|
||||
Generate a nested hierarchical index of length `n` with `len(desc)` levels.
|
||||
Levels are naturally nested, i.e., upper levels have fewer unique values.
|
||||
"""
|
||||
levels = []
|
||||
def index_name(self):
|
||||
"""Return a one-word index name."""
|
||||
return next(self._index_namer)
|
||||
|
||||
# generate lower-level (more detailed) values with full cardinality
|
||||
detailed = self._generate_column(desc[-1], n)
|
||||
levels.insert(0, detailed)
|
||||
def word(self):
|
||||
"""Return a random word (cycles eventually)."""
|
||||
return next(self._word_gen)
|
||||
|
||||
# generate higher levels with fewer unique values
|
||||
for i, dt in enumerate(desc[:-1]):
|
||||
u = 2 if i == 0 else 3
|
||||
unique_vals = self._generate_column(dt, u).unique()
|
||||
repeated = self.rng.choice(unique_vals, size=n, replace=True)
|
||||
levels.insert(0, repeated)
|
||||
|
||||
names = [f"{label_prefix}{j}" for j in range(len(desc))]
|
||||
return pd.MultiIndex.from_arrays(levels, names=names)
|
||||
def tex(self):
|
||||
"""Return a blob of TeX."""
|
||||
return next(self._tex_gen)
|
||||
|
||||
@staticmethod
|
||||
def random_date_within_last_n_years(n: int) -> pd.Timestamp:
|
||||
today = datetime.today()
|
||||
days = random.randint(0, n * 365)
|
||||
return pd.Timestamp(today - timedelta(days=days))
|
||||
|
||||
def _insert_missing(self, df: pd.DataFrame, prop: float) -> pd.DataFrame:
|
||||
"""Insert missing values into dataframe."""
|
||||
if prop <= 0:
|
||||
return df
|
||||
n_rows = df.shape[0]
|
||||
for col in df.columns:
|
||||
n_missing = max(1, int(np.floor(prop * n_rows)))
|
||||
missing_indices = self.rng.choice(n_rows, size=n_missing, replace=False)
|
||||
missing_indices = self.rng.choice(
|
||||
n_rows, size=n_missing, replace=False)
|
||||
df.iloc[missing_indices, df.columns.get_loc(col)] = np.nan
|
||||
return df
|
||||
|
||||
@staticmethod
|
||||
def _is_prime(p: int) -> bool:
|
||||
if p < 2:
|
||||
return False
|
||||
if p == 2:
|
||||
return True
|
||||
if p % 2 == 0:
|
||||
return False
|
||||
for i in range(3, int(p**0.5) + 1, 2):
|
||||
if p % i == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _next_prime(p: int) -> int:
|
||||
if p < 2:
|
||||
return 2
|
||||
p += 1 if p % 2 == 0 else 2 # ensure odd start > p
|
||||
while True:
|
||||
if TestDataFrameFactory._is_prime(p):
|
||||
return p
|
||||
p += 2
|
||||
|
||||
@staticmethod
|
||||
def primes_for_product(n: int, v: int, p0: int) -> list[int]:
|
||||
"""Return a list of distinct primes all >= p0 whose product is >= n."""
|
||||
primes = []
|
||||
p = TestDataFrameFactory._next_prime(max(p0 - 1, 1))
|
||||
while len(primes) < v:
|
||||
primes.append(p)
|
||||
p = TestDataFrameFactory._next_prime(p)
|
||||
|
||||
while prod(primes := sorted(primes)) < n:
|
||||
# increase one level until product is high enough
|
||||
p = TestDataFrameFactory._next_prime(primes[-1])
|
||||
primes[-1] = p
|
||||
# shuffle order
|
||||
random.shuffle(primes)
|
||||
return primes
|
||||
|
||||
def make_index(self, rows: int, levels: int,
|
||||
level_value_lengths: Union[list[int], None] = None,
|
||||
p0: int = 1,
|
||||
padding: int = 2):
|
||||
"""
|
||||
Make an Index with unique values, rows x len(level_value_lengths) cols.
|
||||
|
||||
level_velue_lengths shows how many words long each value should be.
|
||||
padding = over-sample by padding and select sample.
|
||||
"""
|
||||
if level_value_lengths is None:
|
||||
level_value_lengths = random.sample(
|
||||
self.index_value_lengths, levels)
|
||||
else:
|
||||
assert levels == len(
|
||||
level_value_lengths), 'levels must equal len(level_value_lengths)'
|
||||
level_choices = self.primes_for_product(rows * padding, levels, p0=p0)
|
||||
r = [cycle([' '.join([self.word() for _ in range(w)]) for _ in range(k)])
|
||||
for w, k in zip(level_value_lengths, level_choices)]
|
||||
x = [[next(j) for j in r] for i in range(rows)]
|
||||
names = random.sample(name_word_list, levels)
|
||||
idx = pd.MultiIndex.from_tuples(
|
||||
random.sample(x, rows), names=names).sort_values()
|
||||
assert idx.is_unique
|
||||
return idx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,76 +0,0 @@
|
||||
% tex header includes
|
||||
|
||||
|
||||
%\usepackage[T1]{fontenc} % Ensures proper character encoding
|
||||
%\usepackage{textcomp} % Provides additional symbols
|
||||
%\usepackage{newtxtext} % Equivalent to Stix Two Text for text
|
||||
%\usepackage{stix2} % GPT says load here Equivalent to Stix Two Math for math
|
||||
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb} % If needed
|
||||
|
||||
\usepackage{multirow}
|
||||
\usepackage{url}
|
||||
\usepackage{tikz}
|
||||
\usepackage{color}
|
||||
|
||||
%==============================
|
||||
% TikZ Related
|
||||
%==============================
|
||||
\usetikzlibrary{arrows,calc,positioning,shadows.blur,decorations.pathreplacing}
|
||||
\usetikzlibrary{automata}
|
||||
\usetikzlibrary{fit}
|
||||
\usetikzlibrary{snakes}
|
||||
\usetikzlibrary{intersections}
|
||||
\usetikzlibrary{decorations.markings,decorations.text, decorations.pathmorphing,decorations.shapes}
|
||||
\usetikzlibrary{decorations.fractals,decorations.footprints}
|
||||
\usetikzlibrary{graphs}
|
||||
\usetikzlibrary{matrix}
|
||||
\usetikzlibrary{shapes.geometric}
|
||||
\usetikzlibrary{mindmap, shadows}
|
||||
\usetikzlibrary{backgrounds}
|
||||
\usetikzlibrary{cd}
|
||||
|
||||
\newcommand{\grtspacer}{\vphantom{lp}}
|
||||
|
||||
|
||||
%==============================
|
||||
% Float placement customization
|
||||
%==============================
|
||||
\usepackage{float} % Required for \floatplacement and [H] (force here)
|
||||
|
||||
% Default float placement preferences:
|
||||
% These set global default positions for all floats of the given type.
|
||||
|
||||
\floatplacement{table}{h} % Try to place tables "here" by default
|
||||
\floatplacement{figure}{t} % Try to place figures at the top of the page
|
||||
|
||||
% Notes on float specifiers:
|
||||
% h = here (if LaTeX thinks it fits)
|
||||
% t = top of page
|
||||
% b = bottom of page
|
||||
% p = on a separate float-only page
|
||||
% H = exactly here (requires \usepackage{float})
|
||||
|
||||
% You can still override per float:
|
||||
% \begin{table}[htbp] % Try here, then top, bottom, float page
|
||||
% \begin{figure}[H] % Force placement exactly here
|
||||
|
||||
|
||||
%==============================
|
||||
% Listing formats
|
||||
%==============================
|
||||
\usepackage{listings}
|
||||
% size: \tiny, \scriptsize, \footnotesize, \small, \normalsize
|
||||
\lstset{
|
||||
% Uses footnotesize and typewriter font
|
||||
basicstyle=\footnotesize\ttfamily,
|
||||
% Add other listings options here if needed, e.g.,
|
||||
% numbers=left,
|
||||
% numberstyle=\tiny,
|
||||
% keywordstyle=\color{blue},
|
||||
% stringstyle=\color{red},
|
||||
% commentstyle=\color{green},
|
||||
% breaklines=true,
|
||||
% showstringspaces=false,
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,2 +0,0 @@
|
||||
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
% tex header includes
|
||||
|
||||
|
||||
%\usepackage[T1]{fontenc} % Ensures proper character encoding
|
||||
%\usepackage{textcomp} % Provides additional symbols
|
||||
%\usepackage{newtxtext} % Equivalent to Stix Two Text for text
|
||||
%\usepackage{stix2} % GPT says load here Equivalent to Stix Two Math for math
|
||||
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb} % If needed
|
||||
|
||||
\usepackage{multirow}
|
||||
\usepackage{url}
|
||||
\usepackage{tikz}
|
||||
\usepackage{color}
|
||||
|
||||
\usetikzlibrary{arrows,calc,positioning,shadows.blur,decorations.pathreplacing}
|
||||
\usetikzlibrary{automata}
|
||||
\usetikzlibrary{fit}
|
||||
\usetikzlibrary{snakes}
|
||||
\usetikzlibrary{intersections}
|
||||
\usetikzlibrary{decorations.markings,decorations.text, decorations.pathmorphing,decorations.shapes}
|
||||
\usetikzlibrary{decorations.fractals,decorations.footprints}
|
||||
\usetikzlibrary{graphs}
|
||||
\usetikzlibrary{matrix}
|
||||
\usetikzlibrary{shapes.geometric}
|
||||
\usetikzlibrary{mindmap, shadows}
|
||||
\usetikzlibrary{backgrounds}
|
||||
\usetikzlibrary{cd}
|
||||
|
||||
\newcommand{\grtspacer}{\vphantom{lp}}
|
||||
@@ -1,356 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<meta name="author" content="Stephen J. Mildenhall">
|
||||
<meta name="dcterms.date" content="2025-03-14">
|
||||
<title>SINGLE Table</title>
|
||||
<style>
|
||||
code{white-space: pre-wrap;}
|
||||
span.smallcaps{font-variant: small-caps;}
|
||||
div.columns{display: flex; gap: min(4vw, 1.5em);}
|
||||
div.column{flex: auto; overflow-x: auto;}
|
||||
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||
ul.task-list{list-style: none;}
|
||||
ul.task-list li input[type="checkbox"] {
|
||||
width: 0.8em;
|
||||
margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* CSS for syntax highlighting */
|
||||
pre > code.sourceCode { white-space: pre; position: relative; }
|
||||
pre > code.sourceCode > span { line-height: 1.25; }
|
||||
pre > code.sourceCode > span:empty { height: 1.2em; }
|
||||
.sourceCode { overflow: visible; }
|
||||
code.sourceCode > span { color: inherit; text-decoration: inherit; }
|
||||
div.sourceCode { margin: 1em 0; }
|
||||
pre.sourceCode { margin: 0; }
|
||||
@media screen {
|
||||
div.sourceCode { overflow: auto; }
|
||||
}
|
||||
@media print {
|
||||
pre > code.sourceCode { white-space: pre-wrap; }
|
||||
pre > code.sourceCode > span { display: inline-block; text-indent: -5em; padding-left: 5em; }
|
||||
}
|
||||
pre.numberSource code
|
||||
{ counter-reset: source-line 0; }
|
||||
pre.numberSource code > span
|
||||
{ position: relative; left: -4em; counter-increment: source-line; }
|
||||
pre.numberSource code > span > a:first-child::before
|
||||
{ content: counter(source-line);
|
||||
position: relative; left: -1em; text-align: right; vertical-align: baseline;
|
||||
border: none; display: inline-block;
|
||||
-webkit-touch-callout: none; -webkit-user-select: none;
|
||||
-khtml-user-select: none; -moz-user-select: none;
|
||||
-ms-user-select: none; user-select: none;
|
||||
padding: 0 4px; width: 4em;
|
||||
}
|
||||
pre.numberSource { margin-left: 3em; padding-left: 4px; }
|
||||
div.sourceCode
|
||||
{ }
|
||||
@media screen {
|
||||
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script><script src="tables_files/libs/clipboard/clipboard.min.js"></script>
|
||||
<script src="tables_files/libs/quarto-html/quarto.js"></script>
|
||||
<script src="tables_files/libs/quarto-html/popper.min.js"></script>
|
||||
<script src="tables_files/libs/quarto-html/tippy.umd.min.js"></script>
|
||||
<script src="tables_files/libs/quarto-html/anchor.min.js"></script>
|
||||
<link href="tables_files/libs/quarto-html/tippy.css" rel="stylesheet">
|
||||
<link href="tables_files/libs/quarto-html/quarto-syntax-highlighting-01c78b5cd655e4cd89133cf59d535862.css" rel="stylesheet" id="quarto-text-highlighting-styles">
|
||||
<script src="tables_files/libs/bootstrap/bootstrap.min.js"></script>
|
||||
<link href="tables_files/libs/bootstrap/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="tables_files/libs/bootstrap/bootstrap-fcfb2e27d9f44eaf269ffcda1f840c64.min.css" rel="stylesheet" append-hash="true" id="quarto-bootstrap" data-mode="light">
|
||||
<style>html{ scroll-behavior: smooth; }</style>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
|
||||
|
||||
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js" integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg==" crossorigin="anonymous"></script>
|
||||
|
||||
<script type="application/javascript">define('jquery', [],function() {return window.jQuery;})</script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=es6"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js" type="text/javascript"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
const typesetMath = (el) => {
|
||||
if (window.MathJax) {
|
||||
// MathJax Typeset
|
||||
window.MathJax.typeset([el]);
|
||||
} else if (window.katex) {
|
||||
// KaTeX Render
|
||||
var mathElements = el.getElementsByClassName("math");
|
||||
var macros = [];
|
||||
for (var i = 0; i < mathElements.length; i++) {
|
||||
var texText = mathElements[i].firstChild;
|
||||
if (mathElements[i].tagName == "SPAN") {
|
||||
window.katex.render(texText.data, mathElements[i], {
|
||||
displayMode: mathElements[i].classList.contains('display'),
|
||||
throwOnError: false,
|
||||
macros: macros,
|
||||
fleqn: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
window.Quarto = {
|
||||
typesetMath
|
||||
};
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Table 1</h1>
|
||||
|
||||
<div class="greater-table">
|
||||
<style>
|
||||
|
||||
#T25N3COST23ZV {
|
||||
border-collapse: collapse;
|
||||
font-family: "Roboto", "Open Sans Condensed", "Arial", 'Segoe UI', sans-serif;
|
||||
font-size: 0.8em;
|
||||
width: 50em;
|
||||
margin: 10px auto;
|
||||
border: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#T25N3COST23ZV caption { padding: 8px 10px 4px 10px; font-size: 0.88em; text-align: center; font-weight: normal; caption-side: top; }
|
||||
|
||||
#T25N3COST23ZV thead { border-top: 1px solid #000; border-bottom: 1px solid #000; font-size: 0.88em; }
|
||||
|
||||
#T25N3COST23ZV tbody { border-bottom: 1px solid #000; }
|
||||
|
||||
#T25N3COST23ZV th { vertical-align: bottom; padding: 8px 10px 8px 10px; }
|
||||
|
||||
#T25N3COST23ZV td { padding: 4px 10px 4px 10px; vertical-align: top; }
|
||||
|
||||
#T25N3COST23ZV .grt-hrule-0 { border-top: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-hrule-1 { border-top: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-hrule-2 { border-top: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-bhrule-0 { border-bottom: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-bhrule-1 { border-bottom: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-vrule-index { border-left: 1px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-vrule-0 { border-left: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-vrule-1 { border-left: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-vrule-2 { border-left: 0px solid #000; }
|
||||
|
||||
#T25N3COST23ZV .grt-left { text-align: left; }
|
||||
|
||||
#T25N3COST23ZV .grt-center { text-align: center; }
|
||||
|
||||
#T25N3COST23ZV .grt-right { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
|
||||
#T25N3COST23ZV .grt-col-1 {width: 3em;}
|
||||
#T25N3COST23ZV .grt-col-2 {width: 3em;}
|
||||
#T25N3COST23ZV .grt-col-3 {width: 15em;}
|
||||
#T25N3COST23ZV .grt-col-4 {width: 4em;}
|
||||
#T25N3COST23ZV .grt-col-5 {width: 4em;}
|
||||
#T25N3COST23ZV .grt-col-6 {width: 4em;}
|
||||
|
||||
#T25N3COST23ZV .grt-head { font-family: "Times New Roman", 'Courier New'; font-size: 0.88em; }
|
||||
|
||||
#T25N3COST23ZV .grt-bold { font-weight: bold; }</style>
|
||||
<table id="T25N3COST23ZV">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="grt-left">index</th>
|
||||
<th class="grt-center grt-vrule-index" colspan="1">level_0</th>
|
||||
<th class="grt-center grt-vrule-0" colspan="1">level_1</th>
|
||||
<th class="grt-center grt-vrule-0" colspan="1">2025</th>
|
||||
<th class="grt-center grt-vrule-0" colspan="1">2026</th>
|
||||
<th class="grt-center grt-vrule-0" colspan="1">2027</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="grt-col-1 grt-left">0</td>
|
||||
<td class="grt-col-2 grt-left grt-vrule-index">GAAP</td>
|
||||
<td class="grt-col-3 grt-left grt-vrule-0">Underwriting Result</td>
|
||||
<td class="grt-col-4 grt-right grt-vrule-0"></td>
|
||||
<td class="grt-col-5 grt-right grt-vrule-0">-394.81</td>
|
||||
<td class="grt-col-6 grt-right grt-vrule-0"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left">1</td>
|
||||
<td class="grt-left grt-vrule-index">GAAP</td>
|
||||
<td class="grt-left grt-vrule-0">Net Investment Income</td>
|
||||
<td class="grt-right grt-vrule-0"></td>
|
||||
<td class="grt-right grt-vrule-0">60.52</td>
|
||||
<td class="grt-right grt-vrule-0">66.57</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left">2</td>
|
||||
<td class="grt-left grt-vrule-index">GAAP</td>
|
||||
<td class="grt-left grt-vrule-0">Operating Result</td>
|
||||
<td class="grt-right grt-vrule-0"></td>
|
||||
<td class="grt-right grt-vrule-0">-334.29</td>
|
||||
<td class="grt-right grt-vrule-0">66.57</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left">3</td>
|
||||
<td class="grt-left grt-vrule-index">GAAP</td>
|
||||
<td class="grt-left grt-vrule-0">Dividends</td>
|
||||
<td class="grt-right grt-vrule-0"></td>
|
||||
<td class="grt-right grt-vrule-0"></td>
|
||||
<td class="grt-right grt-vrule-0"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h1>Table 2</h1>
|
||||
|
||||
|
||||
<p>Some text above the table.</p>
|
||||
|
||||
<div class="greater-table">
|
||||
<style>
|
||||
|
||||
#TEJECQF5AYPNM {
|
||||
border-collapse: collapse; font-family: "Roboto", "Open Sans Condensed", "Arial", 'Segoe UI', sans-serif;
|
||||
font-size: 0.8em;
|
||||
width: fit-content;
|
||||
/* tb and lr */
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
#TEJECQF5AYPNM caption { padding: 2px 10px 1px 10px; font-size: 0.88em; text-align: center; font-weight: normal; caption-side: top; }
|
||||
|
||||
#TEJECQF5AYPNM thead { border-top: 1px solid #000; border-bottom: 1px solid #000; font-size: 0.88em; }
|
||||
|
||||
#TEJECQF5AYPNM tbody { border-bottom: 1px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM th { vertical-align: bottom; padding: 2px 10px 2px 10px; }
|
||||
|
||||
#TEJECQF5AYPNM td { padding: 1px 10px 1px 10px; vertical-align: top; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-hrule-0 { border-top: 0px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-hrule-1 { border-top: 0px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-hrule-2 { border-top: 0px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-bhrule-0 { border-bottom: 1.5px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-bhrule-1 { border-bottom: 1px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-vrule-index { border-left: 1.5px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-vrule-0 { border-left: 1.5px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-vrule-1 { border-left: 1px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-vrule-2 { border-left: 0.5px solid #000; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-left { text-align: left; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-center { text-align: center; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-right { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-head { font-family: "Times New Roman", 'Courier New'; font-size: 0.88em; }
|
||||
|
||||
#TEJECQF5AYPNM .grt-bold { font-weight: bold; }</style>
|
||||
<table id="TEJECQF5AYPNM" style="float:center">
|
||||
<caption>Table 1. A table with varied column widths.</caption>
|
||||
<colgroup>
|
||||
<col width="250px">
|
||||
<col style="width: 50px">
|
||||
<col style="width: 50px">
|
||||
<col style="width: 50px">
|
||||
<col style="width: 50px">
|
||||
<col style="width: 75px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="grt-left"></th>
|
||||
<th class="grt-center grt-bhrule-0 grt-vrule-index" colspan="2">A</th>
|
||||
<th class="grt-center grt-bhrule-0 grt-vrule-0" colspan="2">B</th>
|
||||
<th class="grt-center grt-bhrule-0 grt-vrule-0" colspan="1">C</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="grt-left">years!</th>
|
||||
<th class="grt-center grt-vrule-index" colspan="1">Int</th>
|
||||
<th class="grt-center grt-vrule-1" colspan="1">Float</th>
|
||||
<th class="grt-center grt-vrule-0" colspan="1">Float</th>
|
||||
<th class="grt-center grt-vrule-1" colspan="1">3</th>
|
||||
<th class="grt-center grt-vrule-0" colspan="1">Longer Text</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="grt-left">2000</td>
|
||||
<td class="grt-right grt-vrule-index">-100,000</td>
|
||||
<td class="grt-right grt-vrule-1"> 2.389p</td>
|
||||
<td class="grt-right grt-vrule-0">-1,601.00</td>
|
||||
<td class="grt-center grt-vrule-1">2025-03-14</td>
|
||||
<td class="grt-left grt-vrule-0">once upon a time, once upon a time, once upon a time, once upon a time</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left grt-hrule-0">2001</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-index">-91,667</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-1"> 22.217p</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-0">-1,367.62</td>
|
||||
<td class="grt-center grt-hrule-0 grt-vrule-1">2025-03-26</td>
|
||||
<td class="grt-left grt-hrule-0 grt-vrule-0"> risk is hard to define</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left grt-hrule-0">2002</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-index">-83,333</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-1"> 206.619p</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-0">-1,134.25</td>
|
||||
<td class="grt-center grt-hrule-0 grt-vrule-1">2025-04-07</td>
|
||||
<td class="grt-left grt-hrule-0 grt-vrule-0"> not in Kansas anymore</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left grt-hrule-0">2003</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-index">-75,000</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-1"> 1.922n</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-0">-900.88</td>
|
||||
<td class="grt-center grt-hrule-0 grt-vrule-1">2025-04-19</td>
|
||||
<td class="grt-left grt-hrule-0 grt-vrule-0"> neutrinos are hard to detect</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left grt-hrule-0">2004</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-index">-66,667</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-1"> 17.870n</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-0">-667.50</td>
|
||||
<td class="grt-center grt-hrule-0 grt-vrule-1">2025-05-01</td>
|
||||
<td class="grt-left grt-hrule-0 grt-vrule-0"> Adam Smith is the father of economics</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grt-left grt-hrule-0">2005</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-index">-58,333</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-1"> 166.196n</td>
|
||||
<td class="grt-right grt-hrule-0 grt-vrule-0">-434.12</td>
|
||||
<td class="grt-center grt-hrule-0 grt-vrule-1">2025-05-13</td>
|
||||
<td class="grt-left grt-hrule-0 grt-vrule-0">once upon a time</td>
|
||||
</tr>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan=3>Footer 1 stuff. This is very long. This is very long. This is very long. This is very long. </td>
|
||||
<td>Footer 2 stuff.</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>Some text below the table.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,199 +0,0 @@
|
||||
/* css styles */
|
||||
.title{
|
||||
text-align: left;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.title.listing-title{
|
||||
font-size: 1.0em;
|
||||
}
|
||||
li.nav-item {
|
||||
text-align: left;
|
||||
}
|
||||
.navbar-title {
|
||||
font-family: Impact, "Arial Narrow", Arial, sans-serif;
|
||||
letter-spacing: -1px;
|
||||
font-size: 1.05em;
|
||||
}
|
||||
.featured-posts {
|
||||
font-size: 1em;
|
||||
/* font-weight: bold;*/
|
||||
/* color: #007BFF; /* Adjust the color to fit your theme */*/
|
||||
}
|
||||
.header-section-number {
|
||||
margin-right: 0.35em;
|
||||
position: relative;
|
||||
}
|
||||
.header-section-number::after {
|
||||
content: ".";
|
||||
color: green; /* Ensure the period is the same color as the number */
|
||||
}
|
||||
figcaption {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #505050;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
body {
|
||||
hyphens: auto;
|
||||
font-family: STIX Two Text, Calisto MT, serif;
|
||||
overflow-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
font-size: 14px;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0 1em 1.7em;
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid #e6e6e6;
|
||||
color: #606060;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0em;
|
||||
margin-bottom: 0.5em;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 1.4em;
|
||||
/*font: sans-serif;*/
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 1.35em;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 0.35em;
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-size: 1.15em;
|
||||
/*font-family: Trebuchet MT, sans-serif;*/
|
||||
}
|
||||
h4 {
|
||||
margin-top: 0.25em;
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-size: 1.0em;
|
||||
/*font-family: Trebuchet MT, sans-serif;*/
|
||||
}
|
||||
h5, h6 {
|
||||
font-family: Helvetica, sans-serif;
|
||||
margin-top: 0em;
|
||||
font-weight: normal;
|
||||
font-size: 0.83em;
|
||||
}
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
font-size: 0.67em;
|
||||
}
|
||||
/*img {
|
||||
max-width: 75%;
|
||||
}*/
|
||||
hr {
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
table {
|
||||
/* margin above below table margin */
|
||||
margin: 1em 0;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
/* table-layout: :auto;*/
|
||||
/*width: 100%;*/
|
||||
}
|
||||
table caption {
|
||||
font-style: italic;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
tbody {
|
||||
margin-top: 0.5em;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
th {
|
||||
border-top: 1px solid #1a1a1a;
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-size: 0.85em;
|
||||
/* top right bottom left */
|
||||
padding: 0.25em 1em 0.25em 1em;
|
||||
}
|
||||
td {
|
||||
padding: 0.125em 1em 0.25em 1em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
ol, ul {
|
||||
padding-left: 1.7em;
|
||||
/*margin-top: 1em;*/
|
||||
}
|
||||
li > ol, li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
li.post_list {
|
||||
margin-bottom: 0.4em;
|
||||
line-height: 1.3em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
code {
|
||||
font-family: Cascadia Mono, Menlo, Monaco, 'Lucida Console', Consolas, monospace;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
color: #a81313;
|
||||
}
|
||||
/* preformatted text */
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.sourceCode {
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
span.smallcaps{font-variant: small-caps;}
|
||||
span.underline{text-decoration: underline;}
|
||||
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||
ul.task-list{list-style: none;}
|
||||
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||
|
||||
|
||||
/* colorize code options */
|
||||
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
|
||||
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
|
||||
code span.at { color: #7d9029; } /* Attribute */
|
||||
code span.bn { color: #40a070; } /* BaseN */
|
||||
code span.bu { } /* BuiltIn */
|
||||
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
|
||||
code span.ch { color: #4070a0; } /* Char */
|
||||
code span.cn { color: #880000; } /* Constant */
|
||||
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
|
||||
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
|
||||
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
|
||||
code span.dt { color: #902000; } /* DataType */
|
||||
code span.dv { color: #40a070; } /* DecVal */
|
||||
code span.er { color: #ff0000; font-weight: bold; } /* Error */
|
||||
code span.ex { } /* Extension */
|
||||
code span.fl { color: #40a070; } /* Float */
|
||||
code span.fu { color: #06287e; } /* Function */
|
||||
code span.im { color: #007020; font-weight: bold; } /* Import */
|
||||
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
|
||||
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
|
||||
code span.op { color: #666666; } /* Operator */
|
||||
code span.ot { color: #007020; } /* Other */
|
||||
code span.pp { color: #bc7a00; } /* Preprocessor */
|
||||
code span.sc { color: #4070a0; } /* SpecialChar */
|
||||
code span.ss { color: #bb6688; } /* SpecialString */
|
||||
code span.st { color: #4070a0; } /* String */
|
||||
code span.va { color: #19177c; } /* Variable */
|
||||
code span.vs { color: #4070a0; } /* VerbatimString */
|
||||
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
|
||||
|
||||
-3194
File diff suppressed because it is too large
Load Diff
-5244
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,412 +0,0 @@
|
||||
---
|
||||
title: All Tables Test - New TestDFGenerator test_suite
|
||||
author:
|
||||
- name: Stephen J. Mildenhall
|
||||
orcid: 0000-0001-6956-0098
|
||||
corresponding: true
|
||||
email: mynl@me.com
|
||||
date: last-modified
|
||||
colorlinks: true
|
||||
link-citations: true
|
||||
link-bibliography: true
|
||||
tbl-align: left
|
||||
number-sections: true
|
||||
number-offset: 0
|
||||
number-depth: 3
|
||||
code-line-numbers: false
|
||||
code-copy: true
|
||||
code-overflow: wrap
|
||||
code-fold: true
|
||||
fig-format: svg
|
||||
fig-align: left
|
||||
format:
|
||||
html:
|
||||
html-table-processing: none
|
||||
theme: litera
|
||||
fontsize: 0.9em
|
||||
css: styles.css
|
||||
include-in-header: pmir-header.html
|
||||
smooth-scroll: true
|
||||
toc-title: 'In this chapter:'
|
||||
citations-hover: true
|
||||
crossrefs-hover: false
|
||||
fig-responsive: true
|
||||
footnotes-hover: true
|
||||
lightbox: true
|
||||
link-external-icon: true
|
||||
link-external-newwindow: true
|
||||
page-layout: article
|
||||
page-navigation: true
|
||||
reference-section-title: ' '
|
||||
page-footer:
|
||||
left: 'Stephen J. Mildenhall. License: [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/).'
|
||||
twitter-card: true
|
||||
open-graph: true
|
||||
toc: true
|
||||
toc-depth: 3
|
||||
math: mathjax
|
||||
pdf:
|
||||
include-in-header: prefobnicate.tex
|
||||
documentclass: scrartcl
|
||||
papersize: a4
|
||||
fontsize: 11pt
|
||||
keep-tex: true
|
||||
geometry: margin=0.8in
|
||||
pdf-engine: lualatex
|
||||
pdf-engine-opts:
|
||||
- '-interaction=nonstopmode'
|
||||
toc: false
|
||||
execute:
|
||||
eval: true
|
||||
echo: true
|
||||
cache: true
|
||||
cache-type: jupyter
|
||||
freeze: false
|
||||
kernel: python3
|
||||
engine: jupyter
|
||||
daemon: 1200
|
||||
jupyter:
|
||||
jupytext:
|
||||
formats: ipynb,qmd:quarto
|
||||
text_representation:
|
||||
extension: .qmd
|
||||
format_name: quarto
|
||||
format_version: '1.0'
|
||||
jupytext_version: 1.16.4
|
||||
kernelspec:
|
||||
display_name: Python 3 (ipykernel)
|
||||
language: python
|
||||
name: python3
|
||||
---
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: setup
|
||||
from IPython.display import HTML, display
|
||||
import matplotlib as mpl
|
||||
import matplotlib.dates as mdates
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import greater_tables as gter
|
||||
import greater_tables.utilities as gtu
|
||||
from greater_tables import GT, sGT
|
||||
gter.logger.setLevel(gter.logging.WARNING)
|
||||
```
|
||||
|
||||
...code build completed.
|
||||
|
||||
# A Hard-Rules table
|
||||
|
||||
Second level index has mixed types. Range of magnitudes. Picking out years.
|
||||
|
||||
\footnotesize
|
||||
|
||||
|
||||
```{python}
|
||||
#| label: tbl-hard-rules
|
||||
#| tbl-cap: Default display output (Quarto generated caption)
|
||||
level_1 = ["A", "A", "B", "B", 'C']
|
||||
level_2 = ['Int', 'Float', 'Float', 3, 'Longer Text']
|
||||
|
||||
multi_index = pd.MultiIndex.from_arrays([level_1, level_2],
|
||||
names=["Level 1", "Level 2"])
|
||||
start = pd.Timestamp.today().normalize() # Today's date, normalized to midnight
|
||||
end = pd.Timestamp(f"{start.year}-12-31") # End of the year
|
||||
|
||||
hard = pd.DataFrame(
|
||||
{'years!': np.arange(2000, 2025, dtype=int),
|
||||
'a': np.array(np.round(np.linspace(-100000, 100000, 25), 0), dtype=int),
|
||||
'b': 9.3 ** np.linspace(-12, 12, 25),
|
||||
'c': np.linspace(-1601, 4000, 25),
|
||||
'd': pd.date_range(start=start, end=end, periods=25),
|
||||
'e': ('once upon a time, risk is hard to define, not in Kansas anymore, '
|
||||
'neutrinos are hard to detect, '
|
||||
'Adam Smith is the father of economics'.split(',') * 5)
|
||||
}).set_index('years!')
|
||||
# hard = hard.head()
|
||||
hard.columns = multi_index
|
||||
hard
|
||||
```
|
||||
|
||||
\normalsize
|
||||
|
||||
@tbl-hard-rules shows the default output and @tbl-hard-rules-2 the `sGT` format output.
|
||||
|
||||
```{python}
|
||||
#| label: tbl-hard-rules-2
|
||||
#| tbl-cap: Greater Tables output (Quarto generated caption)
|
||||
sGT(hard, 'A table with varied columns.')
|
||||
```
|
||||
|
||||
Here are some alternatives:
|
||||
|
||||
* @tbl-hard-rules-3a hrules no vrules
|
||||
* @tbl-hard-rules-3b change date and integer formats and
|
||||
* @tbl-hard-rules-3c change padding and debug mode.
|
||||
|
||||
```{python}
|
||||
#| echo: fenced
|
||||
#| label: tbl-hard-rules-3a
|
||||
#| tbl-cap: No V rules but hrules (Quarto generated caption)
|
||||
display(sGT(hard.sample(5).sort_index(),
|
||||
caption='GT caption No v rules, but h rules',
|
||||
vrule_widths=(0,0,0),
|
||||
hrule_widths=(1,0,0)))
|
||||
```
|
||||
|
||||
```{python}
|
||||
#| echo: fenced
|
||||
#| label: tbl-hard-rules-3b
|
||||
#| tbl-cap: Change date and integer formats (Quarto generated caption)
|
||||
display(sGT(hard.sample(5).sort_index(),
|
||||
caption='Change default date and integer formats',
|
||||
default_date_str='%m-%d', default_integer_str='[{x:d}]'))
|
||||
```
|
||||
|
||||
```{python}
|
||||
#| echo: fenced
|
||||
#| label: tbl-hard-rules-3c
|
||||
#| tbl-cap: Change padding and debug mode, boxes (Quarto generated caption)
|
||||
display(sGT(hard.sample(5).sort_index(),
|
||||
caption='Change padding, debug mode lines',
|
||||
padding_trbl=(10, 10, 20, 20), debug=True))
|
||||
```
|
||||
|
||||
Here is the raw HTML and LaTeX output.
|
||||
|
||||
\footnotesize
|
||||
|
||||
```{python}
|
||||
#| label: raw-output
|
||||
f = sGT(hard.head(4), debug=True)
|
||||
print('HTML output\n')
|
||||
print(f._repr_html_())
|
||||
|
||||
print('\n\n\nTeX output\n')
|
||||
print(f._repr_latex_())
|
||||
```
|
||||
|
||||
\normalsize
|
||||
|
||||
|
||||
# A Table with TeX Content
|
||||
|
||||
```{python}
|
||||
#| label: tbl-tex
|
||||
#| tbl-cap: '(Quarto generated caption): table displayed by default routine.'
|
||||
index = pd.Index(["A", "B", "$C_1$", "C_2 not tex", '$\\cos(A)$'])
|
||||
tex = pd.DataFrame(
|
||||
{'x': np.arange(2020, 2025, dtype=int),
|
||||
'b': np.random.random(5),
|
||||
'a1': [f'$x^{i}$' for i in range(5,10)],
|
||||
'a2': [f'$\\sin({i}x\\pi/n)$' for i in range(5,10)],
|
||||
'a3': [f'$x^{i}$' for i in range(5,10)],
|
||||
'a4': [f'\\(x^{i}\\)' for i in range(5,10)],
|
||||
}).set_index('x')
|
||||
tex = tex.head()
|
||||
tex.columns = index
|
||||
tex
|
||||
```
|
||||
|
||||
```{python}
|
||||
#| label: tbl-tex-2
|
||||
#| tbl-cap: GT output (Quarto generated caption)
|
||||
sGT(tex, 'GT Caption')
|
||||
```
|
||||
|
||||
Ratio columns.
|
||||
|
||||
```{python}
|
||||
#| label: tbl-tex-3
|
||||
#| tbl-cap: greater table output
|
||||
tex.columns = ["A (%)", "B", "$C_1$", "C_2 not tex", '$\\cos(A)$']
|
||||
sGT(tex, 'Ratio columns in A', ratio_cols='A (%)')
|
||||
```
|
||||
|
||||
# Greater_tables Test Suite
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: greater-tables-test
|
||||
test_gen = gtu.TestDFGenerator(0, 0)
|
||||
ans = test_gen.test_suite()
|
||||
```
|
||||
|
||||
## Test Table: basic
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-0
|
||||
#| tbl-cap: GT output for test table basic
|
||||
hrw = (0, 0, 0)
|
||||
sGT(ans['basic'], "Basic", ratio_cols='z', aligners={'w': 'l'},
|
||||
hrule_widths=hrw)
|
||||
```
|
||||
|
||||
Comments go here.
|
||||
|
||||
|
||||
|
||||
## Test Table: timeseries
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-1
|
||||
#| tbl-cap: GT output for test table timeseries
|
||||
hrw = (0, 0, 0)
|
||||
sGT(ans['timeseries'], "Timeseries", ratio_cols='z', aligners={'w': 'l'},
|
||||
hrule_widths=hrw)
|
||||
```
|
||||
|
||||
Comments go here.
|
||||
|
||||
|
||||
|
||||
|
||||
## Test Table: multiindex
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-2
|
||||
#| tbl-cap: GT output for test table multiindex
|
||||
hrw = (1.5, 1.0, 0.5)
|
||||
sGT(ans['multiindex'], "Multiindex", ratio_cols='z', aligners={'w': 'l'},
|
||||
hrule_widths=hrw)
|
||||
```
|
||||
|
||||
Comments go here.
|
||||
|
||||
|
||||
|
||||
|
||||
## Test Table: multicolumns
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-3
|
||||
#| tbl-cap: GT output for test table multicolumns
|
||||
hrw = (0, 0, 0)
|
||||
sGT(ans['multicolumns'], "Multicolumns", ratio_cols='z', aligners={'w': 'l'},
|
||||
hrule_widths=hrw)
|
||||
```
|
||||
|
||||
Comments go here.
|
||||
|
||||
|
||||
|
||||
|
||||
## Test Table: complex
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-4
|
||||
#| tbl-cap: GT output for test table complex
|
||||
hrw = (1.5, 1.0, 0.5)
|
||||
sGT(ans['complex'], "Complex", ratio_cols='z', aligners={'w': 'l'},
|
||||
hrule_widths=hrw)
|
||||
```
|
||||
|
||||
Comments go here.
|
||||
|
||||
# Other input formats
|
||||
|
||||
## Markown
|
||||
|
||||
| **Insured group or insurance product** | **Sat** | **RP** | **RF** |
|
||||
|:------------------------------------------------------------|:-------:|:------:|:------:|
|
||||
| Non-standard auto | x | | |
|
||||
| General liability for judgment proof corporation | x | | |
|
||||
| Term life insurance | | x | |
|
||||
| Catastrophe Reinsurance, outside rating agency bounds | | x | |
|
||||
| High limit property per risk reinsurance | | x | |
|
||||
| Personal lines for affluent individuals | x | x | |
|
||||
| Small commercial lines | x | x | |
|
||||
| Catastrophe reinsurance, within rating agency bounds | x | x | |
|
||||
| Large account captive reinsurance | | | x |
|
||||
| Structured quota share, requiring a risk transfer test | x | | x |
|
||||
| Working layer casualty excess of loss | | x | x |
|
||||
| Surplus relief quota share on cat exposed line | x | x | x |
|
||||
| Middle market commercial lines work comp or commercial auto | x | x | x |
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-5
|
||||
#| tbl-cap: GT from markdown table input
|
||||
txt = '''
|
||||
|
||||
| **Insured group or insurance product** | **Sat** | **RP** | **RF** |
|
||||
|:------------------------------------------------------------|:-------:|:------:|:------:|
|
||||
| Non-standard auto | x | | |
|
||||
| General liability for judgment proof corporation | x | | |
|
||||
| Term life insurance | | x | |
|
||||
| Catastrophe Reinsurance, outside rating agency bounds | | x | |
|
||||
| High limit property per risk reinsurance | | x | |
|
||||
| Personal lines for affluent individuals | x | x | |
|
||||
| Small commercial lines | x | x | |
|
||||
| Catastrophe reinsurance, within rating agency bounds | x | x | |
|
||||
| Large account captive reinsurance | | | x |
|
||||
| Structured quota share, requiring a risk transfer test | x | | x |
|
||||
| Working layer casualty excess of loss | | x | x |
|
||||
| Surplus relief quota share on cat exposed line | x | x | x |
|
||||
| Middle market commercial lines work comp or commercial auto | x | x | x |
|
||||
|
||||
|
||||
'''
|
||||
|
||||
GT(txt)
|
||||
```
|
||||
|
||||
## List of lists
|
||||
|
||||
```{python}
|
||||
x = None
|
||||
if x:
|
||||
print(123)
|
||||
```
|
||||
|
||||
```{python}
|
||||
#| echo: true
|
||||
#| label: tbl-greater-tables-test-6
|
||||
#| tbl-cap: GT output for list of lists input
|
||||
lol = [['a', 'b', 'c', 'd'], ['west', 10, 20, 30], ['east', 10, 200, 30], ['north', 10, 20, 300], ['south', 100, 20, 30]]
|
||||
GT(lol)
|
||||
```
|
||||
|
||||
```{python}
|
||||
f = GT(lol)
|
||||
f
|
||||
```
|
||||
|
||||
```{python}
|
||||
tbl = '''
|
||||
|
||||
Var | Amount
|
||||
:---|------:
|
||||
A | 100.0
|
||||
B | 0.123
|
||||
C | A string
|
||||
|
||||
'''
|
||||
|
||||
def ff(x):
|
||||
if abs(x) < 1:
|
||||
return f'{x:.1%}'
|
||||
else:
|
||||
return f'{x:,.2f}'
|
||||
|
||||
sGT(tbl, table_float_format=ff)
|
||||
```
|
||||
|
||||
```{python}
|
||||
P = 1000 * 1.075**-10 + 120
|
||||
L = 1000
|
||||
ry = .1
|
||||
v = 1/(1+ry)
|
||||
T = 10
|
||||
pv = P - v**T * L
|
||||
fv = pv / v**T
|
||||
pv, fv
|
||||
```
|
||||
|
||||
-1537
File diff suppressed because it is too large
Load Diff
@@ -1,173 +0,0 @@
|
||||
"""Make test tables. Couple of approaches. GPT."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
from random import randint, uniform, sample
|
||||
|
||||
import pandas as pd
|
||||
from faker import Faker
|
||||
|
||||
# Simulate a list of words for column name generation
|
||||
words = [
|
||||
"transaction", "identifier", "processing", "timestamp", "user", "account", "description",
|
||||
"amount", "balance", "location", "currency", "status", "failure", "note", "reference",
|
||||
"operation", "duration", "estimate", "category", "filename", "extension", "type", "project",
|
||||
"client", "supplier", "remark", "address", "email", "comment", "entry", "premium",
|
||||
"loss ratio", 'expense ratio', "combined ratio", 'loss date'
|
||||
]
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
def make_column_name():
|
||||
# choices with replacement -> sample
|
||||
return " ".join(random.sample(words, k=random.randint(1, 5)))
|
||||
|
||||
|
||||
def make_text_blob():
|
||||
return " ".join(sample(words, randint(10, 25)))
|
||||
|
||||
|
||||
def make_test_dataframe(n_rows, n_cols):
|
||||
col_types = random.choices(["int", "float", "str", "date"], k=n_cols)
|
||||
data = {}
|
||||
for _ in range(n_cols):
|
||||
dtype = col_types.pop(0)
|
||||
col_name = make_column_name() + f' ({dtype})'
|
||||
if dtype == "int":
|
||||
data[col_name] = [random.randint(0, 10000) if random.random() > 0.1 else None for _ in range(n_rows)]
|
||||
elif dtype == "float":
|
||||
data[col_name] = [round(random.uniform(0, 1e4), 3) if random.random() > 0.1 else None for _ in range(n_rows)]
|
||||
elif dtype == "str":
|
||||
data[col_name] = [fake.sentence(nb_words=random.randint(2, 8)) if random.random() > 0.1 else None for _ in range(n_rows)]
|
||||
elif dtype == "date":
|
||||
start = datetime(2015, 1, 1)
|
||||
data[col_name] = [
|
||||
(start + timedelta(days=random.randint(0, 4000))).date().isoformat()
|
||||
if random.random() > 0.1 else None for _ in range(n_rows)
|
||||
]
|
||||
return pd.DataFrame(data)
|
||||
|
||||
|
||||
def make_dataframe_set(n):
|
||||
"""Sample dataframes with n rows."""
|
||||
def rand_date():
|
||||
start = datetime(2000, 1, 1)
|
||||
return [(start + timedelta(days=randint(0, 10000))).strftime("%Y-%m-%d") for _ in range(n)]
|
||||
|
||||
def rand_float():
|
||||
return [f"{uniform(0, 10000):.3f}" for _ in range(n)]
|
||||
|
||||
def rand_int():
|
||||
return [str(randint(0, 5000)) for _ in range(n)]
|
||||
|
||||
def rand_text():
|
||||
return [make_text_blob() for _ in range(n)]
|
||||
|
||||
def rand_filename():
|
||||
return [f"{'_'.join(sample(words, randint(2, 5)))}.pdf" for _ in range(n)]
|
||||
|
||||
def col(colfunc, allow_missing=False):
|
||||
vals = colfunc()
|
||||
if allow_missing:
|
||||
for i in range(randint(1, 3)):
|
||||
vals[randint(0, len(vals) - 1)] = ''
|
||||
return vals
|
||||
|
||||
dfs = {}
|
||||
dfs["floats dates filenames"] = pd.DataFrame({
|
||||
make_column_name(): col(rand_float),
|
||||
make_column_name(): col(rand_date),
|
||||
make_column_name(): col(rand_filename),
|
||||
make_column_name(): col(rand_int),
|
||||
make_column_name(): col(rand_float, allow_missing=True),
|
||||
})
|
||||
|
||||
dfs["dense text and numbers"] = pd.DataFrame({
|
||||
make_column_name(): col(rand_text),
|
||||
make_column_name(): col(rand_float),
|
||||
make_column_name(): col(rand_int),
|
||||
make_column_name(): col(rand_text),
|
||||
make_column_name(): col(rand_date),
|
||||
make_column_name(): col(rand_float, allow_missing=True),
|
||||
})
|
||||
|
||||
dfs["mixed data with missing"] = pd.DataFrame({
|
||||
make_column_name(): col(rand_float, allow_missing=True),
|
||||
make_column_name(): col(rand_text, allow_missing=True),
|
||||
make_column_name(): col(rand_int, allow_missing=True),
|
||||
make_column_name(): col(rand_date, allow_missing=True),
|
||||
make_column_name(): col(rand_filename, allow_missing=True),
|
||||
})
|
||||
|
||||
dfs["long header names"] = pd.DataFrame({
|
||||
"Detailed Instrumentation Configuration Summary": col(rand_text),
|
||||
"Archive Metadata Extraction Date Field": col(rand_date),
|
||||
"Overview Record Approximation Notes": col(rand_text),
|
||||
"Velocity Gradient Approximation Float": col(rand_float),
|
||||
"Pressure Summary Int Field": col(rand_int),
|
||||
})
|
||||
|
||||
dfs["file-centric record"] = pd.DataFrame({
|
||||
make_column_name(): col(rand_filename),
|
||||
make_column_name(): col(rand_date),
|
||||
make_column_name(): col(rand_text),
|
||||
make_column_name(): col(rand_float),
|
||||
make_column_name(): col(rand_int),
|
||||
make_column_name(): col(rand_date),
|
||||
make_column_name(): col(rand_filename, allow_missing=True),
|
||||
})
|
||||
|
||||
return dfs
|
||||
|
||||
|
||||
def make_manual_tests():
|
||||
"""Five handwritten test tables."""
|
||||
df1 = pd.DataFrame({
|
||||
"Consideration of Consequences": ["A rather long text value that could wrap badly.", "Short", "A second problematic entry with spaces."],
|
||||
"Probability": ["Likely", "Unlikely", "Moderate"],
|
||||
"Expected Value": ["High", "Low", "Moderate"]
|
||||
})
|
||||
|
||||
df2 = pd.DataFrame({
|
||||
"event_date": ["2024-12-28", "2025-01-05", "2031-06-21"],
|
||||
"timestamp": ["2024-12-28T14:23:00", "2025-01-05T09:12:45", "2031-06-21T23:59:59"],
|
||||
"transaction_code": ["ABC-1001-ZZ", "XYZ-2048-AA", "LONG-CODE-2025-EXTREME"]
|
||||
})
|
||||
|
||||
df3 = pd.DataFrame({
|
||||
"notes": [
|
||||
"Item 1: delivered; ready for invoice.",
|
||||
"Warning -- unit may be faulty?",
|
||||
"Check: power supply (see page 42)"
|
||||
],
|
||||
"status": ["✓", "✗", "↺"],
|
||||
"path": [
|
||||
"/usr/local/bin/run.sh",
|
||||
"C:\\Program Files\\App\\main.exe",
|
||||
"~/Documents/projects/final-report.pdf"
|
||||
]
|
||||
})
|
||||
|
||||
df4 = pd.DataFrame({
|
||||
"Serial": ["A123B456", "X987Y654", "Z000Z111"],
|
||||
"MD5 Hash": [
|
||||
"a5c3e1d7f2b9c3d6f1e4a9b3c7d1e2f3",
|
||||
"9f1c4d3e7a6b2d5c8e3f9a1b7c6d4e5f",
|
||||
"ffb1a2c3d4e5f67890123456789abcdef"
|
||||
],
|
||||
"Unwrapped": ["SingleLineValue", "AnotherOne", "NoBreaksHere"]
|
||||
})
|
||||
|
||||
arrays = [
|
||||
["Simulation", "Simulation", "Input", "Input", "Output"],
|
||||
["ID", "Date Generated", "Model Name", "Parameters", "Result Summary"]
|
||||
]
|
||||
columns = pd.MultiIndex.from_arrays(arrays)
|
||||
df5 = pd.DataFrame([
|
||||
[1, "2024-11-15", "RiskModelV2", "α=0.95, β=3.2", "Stable. 5 iterations. RMSE=0.003"],
|
||||
[2, "2025-02-04", "SuperModel", "α=0.99, β=2.1", "Converged quickly. RMSE=0.001"],
|
||||
[3, "2026-08-12", "LongModelNameWithDetails", "α=0.90, β=4.0, γ=1.0", "Diverged on step 4. RMSE=N/A"]
|
||||
], columns=columns)
|
||||
|
||||
return [df1, df2, df3, df4, df5]
|
||||
@@ -1,348 +0,0 @@
|
||||
import datetime as dt
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
import random
|
||||
import sys
|
||||
from IPython.display import HTML, display
|
||||
|
||||
from . greater_tables import GT
|
||||
|
||||
# GPT recommended approach
|
||||
logger = logging.getLogger(__name__)
|
||||
# Disable log propagation to prevent duplicates
|
||||
logger.propagate = False
|
||||
if logger.hasHandlers():
|
||||
# Clear existing handlers
|
||||
logger.handlers.clear()
|
||||
# SET DEGBUUGER LEVEL
|
||||
LEVEL = logging.WARNING # DEBUG or INFO, WARNING, ERROR, CRITICAL
|
||||
logger.setLevel(LEVEL)
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setLevel(LEVEL)
|
||||
formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(funcName)-15s | %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.info(f'Logger Setup; {__name__} module recompiled.')
|
||||
|
||||
|
||||
def write_all_tables(out_path='\\s\\telos\\pmir_studynote\\quarto_scratch\\tables.qmd'):
|
||||
"""Write a tester for all tables to a qmd file."""
|
||||
header = '''---
|
||||
title: {title}
|
||||
format:
|
||||
html:
|
||||
html-table-processing: none
|
||||
pdf:
|
||||
include-in-header: prefobnicate.tex
|
||||
---
|
||||
|
||||
# Set up code
|
||||
|
||||
```{{python}}
|
||||
#| echo: true
|
||||
#| label: setup
|
||||
%run prefobnicate.py
|
||||
import proformas as pf
|
||||
|
||||
import greater_tables as gter
|
||||
import greater_tables.utilities as gtu
|
||||
gter.logger.setLevel(gter.logging.WARNING)
|
||||
from IPython.display import display
|
||||
|
||||
```
|
||||
|
||||
...code build completed.
|
||||
|
||||
# Greater_tables Output
|
||||
|
||||
```{{python}}
|
||||
#| echo: true
|
||||
#| label: greater-tables-test
|
||||
test_gen = gtu.TestDFGenerator()
|
||||
ans = test_gen.test_suite()
|
||||
```
|
||||
|
||||
'''
|
||||
template = '''
|
||||
|
||||
## Test Table {k}
|
||||
|
||||
```{{python}}
|
||||
#| echo: fold
|
||||
#| label: tbl-greater-tables-test-{i}
|
||||
#| tbl-cap: Output for test table {k}
|
||||
hrw = {hrw}
|
||||
f = gter.GT(ans['{k}'], "{title}", ratio_cols='z', aligners={{'w': 'l'}},
|
||||
hrule_widths=hrw)
|
||||
h = f._repr_html_()
|
||||
print(f.df.dtypes)
|
||||
h
|
||||
```
|
||||
|
||||
Comments go here.
|
||||
|
||||
'''
|
||||
tdf = TestDFGenerator()
|
||||
ans = tdf.test_suite()
|
||||
out = [header.format(title='All Tables Test - New TestDFGenerator test_suite')]
|
||||
for i, (k, v) in enumerate(ans.items()):
|
||||
if v.index.nlevels > 1:
|
||||
hrw = (1.5, 1.0, 0.5)
|
||||
else:
|
||||
hrw = (0,0,0)
|
||||
out.append(template.format(i=i, k=k, hrw=hrw, title=k.title()))
|
||||
|
||||
p = Path(out_path)
|
||||
p.write_text('\n'.join(out), encoding='utf-8')
|
||||
|
||||
|
||||
# ==================================================
|
||||
# SUPER DOOPER test df generator with help from GPT
|
||||
class TestDFGenerator:
|
||||
"""Make excellent test DataFrames."""
|
||||
# Load a list of words
|
||||
_word_list_path = 'C:\\s\\Websites\\new_mynl\\word_lists\\match 12.md'
|
||||
_word_list_url = 'https://www.mynl.com/static/words.csv'
|
||||
_word_list = None
|
||||
|
||||
def __init__(self, nan_proportion=0.05, missing_proportion=0,
|
||||
title=False, sep='_', file_path='local'):
|
||||
"""Initialise the generator."""
|
||||
self.nan_proportion = nan_proportion
|
||||
self.missing_proportion = missing_proportion
|
||||
self.title = title # whether to apply title to col names
|
||||
self.sep = sep # separator for column names
|
||||
if TestDFGenerator._word_list is None:
|
||||
TestDFGenerator._word_list = TestDFGenerator.load_words(file_path)
|
||||
# control datatypes
|
||||
self.data_types = ["int", "float", "str", "year", "date", 'datetime']
|
||||
# types:
|
||||
self.index_probs = np.array([20, 1, 20, 45, 12, 5], dtype=float)
|
||||
self.index_probs /= self.index_probs.sum()
|
||||
# control datatypes, types as above
|
||||
self.data_type_probs = np.array([1, 2, 0.5, 0.5, 0.5, 0.5], dtype=float)
|
||||
self.data_type_probs /= self.data_type_probs.sum()
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation."""
|
||||
return f"TestDFGenerator({len(self.words):,d} words)"
|
||||
|
||||
@staticmethod
|
||||
def load_words(file_path=''):
|
||||
"""Load a list of words from a file."""
|
||||
if file_path == 'local':
|
||||
file_path = TestDFGenerator._word_list_path
|
||||
if file_path != '':
|
||||
p = Path(file_path)
|
||||
txt = p.read_text(encoding='utf-8')
|
||||
wl = txt.split('\n')
|
||||
else:
|
||||
wl = pd.read_csv(TestDFGenerator._word_list_url, header=None)[0].values
|
||||
logger.info(f"Loaded wordlist.") # Debug print
|
||||
return wl
|
||||
|
||||
@property
|
||||
def words(self):
|
||||
"""Return the word list."""
|
||||
random.shuffle(self._word_list)
|
||||
return self._word_list
|
||||
|
||||
def make_column_names(self, n, g):
|
||||
"""Make n column names each g words long."""
|
||||
if self.title:
|
||||
return [self.sep.join(x).title() for x in zip(*[iter(self.words[:n * g])] * g)]
|
||||
else:
|
||||
return [self.sep.join(x) for x in zip(*[iter(self.words[:n * g])] * g)]
|
||||
|
||||
def make_index_data(self, dtype, size):
|
||||
"""Generate index values with natural nesting."""
|
||||
if dtype == "int":
|
||||
values = np.random.randint(0, 100000, size=size)
|
||||
elif dtype == "float":
|
||||
values = np.random.uniform(-1e6, 1e6, size=size).round(2)
|
||||
elif dtype == "str":
|
||||
values = np.random.choice(self.words, size=size)
|
||||
elif dtype == 'year':
|
||||
values = np.random.choice(np.arange(1990, 2030, dtype=int), size=size, replace=False)
|
||||
elif dtype == "date":
|
||||
start_date = datetime(2020, 1, 1)
|
||||
values = [start_date + timedelta(days=random.randint(-5000, 5000)) for _ in range(size)]
|
||||
elif dtype == "datetime":
|
||||
start_date = datetime(2020, 1, 1)
|
||||
values = [start_date + timedelta(days=random.randint(-5000, 5000),
|
||||
hours=random.randint(0, 23),
|
||||
minutes=random.randint(0, 59),
|
||||
seconds=random.randint(0, 59),
|
||||
microseconds=random.randint(0, 999999))
|
||||
for _ in range(size)]
|
||||
return values # noqa
|
||||
|
||||
def make_multi_index(self, dtypes, levels, size):
|
||||
"""Generate a MultiIndex with natural nesting."""
|
||||
# lowest level of index
|
||||
detailed_index = self.make_index_data(dtypes[-1], size)
|
||||
# now make the higher levels, here we want far fewer unique values to make repeats
|
||||
higher_levels = []
|
||||
for i in range(levels - 1):
|
||||
# at level i have i + 2 types?? no just go with 3
|
||||
sample = self.make_index_data(dtypes[i], 2 if i==0 else 3)
|
||||
higher_levels.append(np.random.choice(sample, size=size))
|
||||
index_names = np.random.choice(self.words, levels, replace=False)
|
||||
return pd.MultiIndex.from_arrays([*higher_levels, detailed_index], names=index_names)
|
||||
|
||||
def make_column_data(self, dtype, size):
|
||||
"""Generate column data based on type."""
|
||||
if dtype == "int":
|
||||
picker = np.random.rand()
|
||||
if picker < 0.5:
|
||||
return np.random.randint(-10000, 10000, size=size)
|
||||
else:
|
||||
return np.random.randint(0, 10**9, size=size)
|
||||
elif dtype == "float":
|
||||
picker = np.random.rand()
|
||||
if picker < 0.4:
|
||||
return 10. ** np.random.uniform(-9, 1, size=size)
|
||||
elif picker < 0.8:
|
||||
return 10. ** np.random.uniform(-1, 10, size=size)
|
||||
else:
|
||||
signs = np.random.choice([-1, 1], size=size)
|
||||
return np.pi ** np.random.uniform(-75, 75, size=size) * signs
|
||||
elif dtype == "str":
|
||||
return np.random.choice(self.words, size=size)
|
||||
elif dtype == 'year':
|
||||
return np.random.choice(range(1990, 2030), size=size)
|
||||
elif dtype == "date":
|
||||
start_date = datetime(2020, 1, 1)
|
||||
dates = [start_date + timedelta(days=random.randint(-5000, 5000)) for _ in range(size)]
|
||||
return pd.to_datetime(np.random.choice([d.strftime("%Y-%m-%d") for d in dates], size=size))
|
||||
elif dtype == "datetime":
|
||||
start_date = datetime(2020, 1, 1)
|
||||
dates = [start_date + timedelta(days=random.randint(-5000, 5000),
|
||||
hours=random.randint(0, 23),
|
||||
minutes=random.randint(0, 59),
|
||||
seconds=random.randint(0, 59),
|
||||
microseconds=random.randint(0, 999999))
|
||||
for _ in range(size)]
|
||||
return pd.to_datetime(np.random.choice([d.strftime("%Y-%m-%d %H:%M:%S.%f") for d in dates], size=size))
|
||||
|
||||
def make_test_dataframe(self,
|
||||
num_rows=10,
|
||||
num_columns=5,
|
||||
num_index_levels=1,
|
||||
num_column_levels=1,
|
||||
column_name_length=3,
|
||||
dtype_label=True,
|
||||
index_types=None,
|
||||
title=False,
|
||||
sep='_'
|
||||
):
|
||||
"""
|
||||
Generate a random pandas DataFrame with diverse structures for testing.
|
||||
|
||||
Parameters:
|
||||
- num_rows (int): Number of rows.
|
||||
- num_columns (int): Number of columns.
|
||||
- num_index_levels (int): Levels in the index (1+).
|
||||
- num_column_levels (int): Levels in the columns (1+).
|
||||
- column_name_length (int): Words per column name.
|
||||
- dtype_label (bool): Whether to tag columns with their type.
|
||||
- index_types (list): List of index data types for each level.
|
||||
- words (list): List of words for generating column names.
|
||||
|
||||
Returns:
|
||||
- pd.DataFrame: A test DataFrame with diverse structures.
|
||||
"""
|
||||
# update
|
||||
self.title = title
|
||||
self.sep = sep
|
||||
# Generate column names
|
||||
col_names = self.make_column_names(num_columns,
|
||||
max(1, column_name_length - (1 if dtype_label else 0)))
|
||||
|
||||
# Randomly select index data types for each level
|
||||
if index_types is None:
|
||||
index_types = np.random.choice(self.data_types, num_index_levels, p=self.index_probs, replace=True)
|
||||
if not isinstance(index_types, (tuple, list)):
|
||||
index_types = [index_types]
|
||||
if len(index_types) < num_index_levels:
|
||||
# well...
|
||||
index_types = (index_types * 10)[:num_index_levels]
|
||||
|
||||
# Generate hierarchical MultiIndex with natural grouping
|
||||
if num_index_levels > 1:
|
||||
index = self.make_multi_index(index_types, num_index_levels, num_rows)
|
||||
else:
|
||||
name = np.random.choice(self.words, 1)[0]
|
||||
index = pd.Index(self.make_index_data(index_types[0], num_rows), name=name)
|
||||
|
||||
# Data types
|
||||
dtype_choices = np.random.choice(self.data_types, num_columns, p=self.data_type_probs, replace=True)
|
||||
|
||||
# Generate column structure
|
||||
if num_column_levels > 1:
|
||||
columns = self.make_multi_index(['str'] * num_column_levels, num_column_levels, num_columns)
|
||||
# don't want the index names
|
||||
columns.names = [''] * num_column_levels
|
||||
else:
|
||||
columns = pd.Index([f"{col} {dtype}" if dtype_label else col
|
||||
for col, dtype in zip(col_names, dtype_choices)],
|
||||
name="Column")
|
||||
|
||||
# Generate data
|
||||
data = {col: self.make_column_data(dtype, num_rows) for col, dtype in zip(columns, dtype_choices)}
|
||||
df = pd.DataFrame(data, index=index, columns=columns)
|
||||
|
||||
# Convert date columns to datetime dtype
|
||||
for col, dtype in zip(columns, dtype_choices):
|
||||
if dtype == "date":
|
||||
df[col] = pd.to_datetime(df[col], errors="coerce")
|
||||
|
||||
# Introduce NaNs
|
||||
num_nans = int(self.nan_proportion * num_rows * num_columns)
|
||||
for _ in range(num_nans):
|
||||
df.iat[random.randint(0, num_rows - 1), random.randint(0, num_columns - 1)] = np.nan
|
||||
|
||||
# Introduce radical None values
|
||||
if self.missing_proportion:
|
||||
num_missing = int(self.missing_proportion * num_rows * num_columns)
|
||||
for _ in range(num_missing):
|
||||
df.iat[random.randint(0, num_rows - 1), random.randint(0, num_columns - 1)] = None
|
||||
|
||||
df = df.sort_index().sort_index(axis=1)
|
||||
return df
|
||||
|
||||
__call__ = make_test_dataframe
|
||||
|
||||
def test_suite(self):
|
||||
"""Make a dict of test dataframes with different characteristics."""
|
||||
ans = {}
|
||||
|
||||
ans['basic'] = self.make_test_dataframe(num_rows=10, num_columns=8,
|
||||
num_index_levels=1, num_column_levels=1,
|
||||
column_name_length=1,
|
||||
index_types=['int'])
|
||||
|
||||
ans['timeseries'] = self.make_test_dataframe(num_rows=20, num_columns=3,
|
||||
num_index_levels=1, num_column_levels=1,
|
||||
column_name_length=4, title=True, sep=' ',
|
||||
index_types=['datetime'])
|
||||
|
||||
ans['multiindex'] = self.make_test_dataframe(num_rows=10, num_columns=5,
|
||||
num_index_levels=3, num_column_levels=1,
|
||||
column_name_length=4, title=True, sep=' ',
|
||||
index_types=['int', 'str'])
|
||||
|
||||
ans['multicolumns'] = self.make_test_dataframe(num_rows=10, num_columns=5,
|
||||
num_index_levels=1, num_column_levels=3,
|
||||
column_name_length=4, title=True, sep=' ',
|
||||
index_types=['int', 'str'])
|
||||
|
||||
ans['complex'] = self.make_test_dataframe(num_rows=20, num_columns=10,
|
||||
num_index_levels=3, num_column_levels=3,
|
||||
column_name_length=4,
|
||||
index_types=['int', 'str'])
|
||||
|
||||
return ans
|
||||
Reference in New Issue
Block a user