README tidying; testdf massive...

This commit is contained in:
Stephen Mildenhall
2025-06-13 23:41:59 +01:00
parent 167ce5c919
commit ec3ac66552
23 changed files with 32146 additions and 12170 deletions
-108
View File
@@ -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., 18002100)
"""
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
```
+49 -2
View File
@@ -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
![](https://img.shields.io/readthedocs/greater_tables_project)
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
View File
@@ -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.')
```
![HTML output](hard_table_html.png)
![TeX output](hard_table_tex.png)
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
View File
@@ -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
+5 -38
View File
@@ -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
View File
@@ -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
"""
+9
View File
@@ -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
View File
@@ -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
-76
View File
@@ -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

-2
View File
@@ -1,2 +0,0 @@
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
-31
View File
@@ -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}}
-356
View File
@@ -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>
-199
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-5244
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
-412
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-173
View File
@@ -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]
-348
View File
@@ -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