diff --git a/docs/versions.rst b/docs/versions.rst index f2ead92..6d0704c 100644 --- a/docs/versions.rst +++ b/docs/versions.rst @@ -11,6 +11,10 @@ Versions and Change Log .. TODO * self.padl and r / 12 in make html width adj s/b elsewhere +5.2.1 +----- +* Added ``SmartTitle`` class in utilites - eventually add capitalize option for index? +* Added ``tikz`` option: if true compute tikz output else when known not to be needed. 5.2.0 ------ diff --git a/greater_tables/__init__.py b/greater_tables/__init__.py index 776065a..c813539 100644 --- a/greater_tables/__init__.py +++ b/greater_tables/__init__.py @@ -3,5 +3,5 @@ __author__ = 'Stephen J Mildenhall' __version__ = '5.2.1' from . core import GT -from . fabrications import Fabricator +from . fabrications import * from . etcher import Etcher diff --git a/greater_tables/config.py b/greater_tables/config.py index c3640c5..0e5e9b9 100644 --- a/greater_tables/config.py +++ b/greater_tables/config.py @@ -54,7 +54,6 @@ class Configurator(BaseModel): default_formatter: Optional[Union[str, Callable[[Any, str], str]]] = Field( None, description="Optional fallback formatter f-string" ) - table_float_format: Optional[Union[str, Callable[[Any, str], str]]] = Field( None, description="Float format function or format string for the entire table; overrides column-specific formats" ) @@ -157,6 +156,9 @@ class Configurator(BaseModel): ) # tikz specific options + tikz: bool = Field( + True, description="Compute tikz output (default), else skipped for speed." + ) tikz_scale: float = Field( 1.0, description="Scaling factor applied to LaTeX TikZ tables" ) diff --git a/greater_tables/core.py b/greater_tables/core.py index deb8102..1657a09 100644 --- a/greater_tables/core.py +++ b/greater_tables/core.py @@ -45,6 +45,7 @@ warnings.simplefilter(action='ignore', category=FutureWarning) # GPT recommended approach logger = logging.getLogger(__name__) + class GT(object): """ Create a greater_tables formatting object. @@ -100,101 +101,6 @@ class GT(object): * Sparsify if requested and if multiindex * Result is a dataframe with all object column types and values that reflect the formatting choices. - - - Parameters - ----------- - - :param df: target DataFrame or list of lists or markdown table string - :param caption: table caption, optional (GT will look for gt_caption - attribute of df and use that) - :param label: TeX label (used in \\label{} command). For markdown - tables with #tbl:... in the caption it is extracted automatically. - :param aligners: None or dict (type or colname) -> left | center | - right - :param formatters: None or dict (type or colname) -> format function - for the column; formatters trump ratio_cols - :param unbreakable: None or list of columns to be considered unbreakable - :param ratio_cols: None, or "all" or list of column names treated as - ratios. Set defaults in derived class suitable to application. - :param year_cols: None, or "all" or list of column names treated as - years (no commas, no decimals). Set defaults in derived class suitable - to application. - :param date_cols: None, or "all" or list of column names treated as - dates. Set defaults in derived class suitable to application. - :param raw_cols: None, or "all" or list of column names that are NOT - cast to floats. Set defaults in derived class suitable to application. - :param show_index: if True, show the index columns, default True - :param config.default_integer_str: format f-string for integers, default - value '{x:,d}' - :param config.default_float_str: format f-string for floats, default - value '{x:,.3f}' - :param config.default_date_str: format f-string for dates, default '%Y-%m-%d'. - NOTE: no braces or x! - :param config.default_ratio_str: format f-string for ratios, default '{x:.1%}' - :param config.table_float_format: None or format string for floats in the - table format function, applied to entire table, default None - :param config.table_hrule_width: width of the table top, botton and header - hrule, default 1 - :param config.table_vrule_width: width of the table vrule, separating the - index from the body, default 1 - :param config.hrule_widths: None or tuple of three ints for hrule widths - (for use with multiindexes) - :param config.vrule_widths: None or tuple of three ints for vrule widths - (for use when columns have multiindexes) - :param config.sparsify: if True, config.sparsify the index columns, you almost always - want this to be true! - :param config.sparsify_columns: if True, config.sparsify the columns, default True, - generally a better look, headings centered in colspans - :param config.spacing: 'tight', 'medium', 'wide' to quickly set cell padding. - Medium is default (2, 10, 2, 10). - :param config.padding_trbl: None or tuple of four ints for padding, in order - top, right, bottom, left. - :param config.tikz_scale: scale factor applied to tikz LaTeX tables. - :param config.font_body: font size for body text, default 0.9. Units in em. - :param config.font_head: font size for header text, default 1.0. Units in em. - :param config.font_caption: font size for caption text, default 1.1. - Units in em. - :param config.font_bold_index: if True, make the index columns bold, - default False. - :param config.pef_precision: precision (digits after period) for pandas - engineering format, default 3. - :param config.pef_lower: apply engineering format to floats with absolute - value < 10**config.pef_lower; default -3. - :param config.pef_upper: apply engineering format to floats with absolute - value > 10**config.pef_upper; default 6. - :param config.cast_to_floats: if True, try to cast all non-integer, non-date - columns to floats - :param config.header_row: True: use first row as headers; False no headings. - Default True - :param config.tabs: None or list of column widths in characters or a common - int or float width. (It is converted into em; one character is about - 0.5em on average; digits are exactly 0.5em.) If None, will be calculated. - Default None. - :param config.equal: if True, set all column widths config.equal. Default False. Maybe - ignored, depending on computed ideal column widths. - :param config.caption_align: for the caption - :param config.large_ok: signal that you are intentionally applying to a large - dataframe. Sub-classes may restrict or apply .head() to df. - :param config.max_str_length: maximum displayed length of object types, that - are cast to strings. Eg if you have nested DataFrames! - :param str_table_fmt: table border format used for string output - (markdown), default mixed_grid DEPRECATED?? - :param config.table_width_mode: - 'explicit': set using config.max_table_width_em - 'natural': each cell on one line (can be very wide with long strings) - 'breakable': wrap breakable cells (text strings) at word boundaries - to fit longest word - 'minimum': wrap breakable and ok-to-break (dates) cells - :param config.table_width_header_adjust: additional proportion of table width - used to balance header columns. - :param config.table_width_header_relax: extra spaces allowed per column heading - to facilitate better column header wrapping. - :param config.max_table_width_em: max table width used for markdown string output, - default 200; width is never less than minimum width. Padding (3 chars - per row plus 1) consumed out of config.max_table_width_em in string output mode. - :param config.debug: if True, add id to caption and use colored lines in table, - default False. """ def __init__( @@ -217,6 +123,100 @@ class GT(object): config_path: Path | None = None, **overrides, ): + """ + Available keyword ``**overrides`: + + :param df: target DataFrame or list of lists or markdown table string + :param caption: table caption, optional (GT will look for gt_caption + attribute of df and use that) + :param label: TeX label (used in \\label{} command). For markdown + tables with #tbl:... in the caption it is extracted automatically. + :param aligners: None or dict (type or colname) -> left | center | + right + :param formatters: None or dict (type or colname) -> format function + for the column; formatters trump ratio_cols + :param unbreakable: None or list of columns to be considered unbreakable + :param ratio_cols: None, or "all" or list of column names treated as + ratios. Set defaults in derived class suitable to application. + :param year_cols: None, or "all" or list of column names treated as + years (no commas, no decimals). Set defaults in derived class suitable + to application. + :param date_cols: None, or "all" or list of column names treated as + dates. Set defaults in derived class suitable to application. + :param raw_cols: None, or "all" or list of column names that are NOT + cast to floats. Set defaults in derived class suitable to application. + :param show_index: if True, show the index columns, default True + :param config.default_integer_str: format f-string for integers, default + value '{x:,d}' + :param config.default_float_str: format f-string for floats, default + value '{x:,.3f}' + :param config.default_date_str: format f-string for dates, default '%Y-%m-%d'. + NOTE: no braces or x! + :param config.default_ratio_str: format f-string for ratios, default '{x:.1%}' + :param config.table_float_format: None or format string for floats in the + table format function, applied to entire table, default None + :param config.table_hrule_width: width of the table top, botton and header + hrule, default 1 + :param config.table_vrule_width: width of the table vrule, separating the + index from the body, default 1 + :param config.hrule_widths: None or tuple of three ints for hrule widths + (for use with multiindexes) + :param config.vrule_widths: None or tuple of three ints for vrule widths + (for use when columns have multiindexes) + :param config.sparsify: if True, config.sparsify the index columns, you almost always + want this to be true! + :param config.sparsify_columns: if True, config.sparsify the columns, default True, + generally a better look, headings centered in colspans + :param config.spacing: 'tight', 'medium', 'wide' to quickly set cell padding. + Medium is default (2, 10, 2, 10). + :param config.padding_trbl: None or tuple of four ints for padding, in order + top, right, bottom, left. + :param config.tikz_scale: scale factor applied to tikz LaTeX tables. + :param config.font_body: font size for body text, default 0.9. Units in em. + :param config.font_head: font size for header text, default 1.0. Units in em. + :param config.font_caption: font size for caption text, default 1.1. + Units in em. + :param config.font_bold_index: if True, make the index columns bold, + default False. + :param config.pef_precision: precision (digits after period) for pandas + engineering format, default 3. + :param config.pef_lower: apply engineering format to floats with absolute + value < 10**config.pef_lower; default -3. + :param config.pef_upper: apply engineering format to floats with absolute + value > 10**config.pef_upper; default 6. + :param config.cast_to_floats: if True, try to cast all non-integer, non-date + columns to floats + :param config.header_row: True: use first row as headers; False no headings. + Default True + :param config.tabs: None or list of column widths in characters or a common + int or float width. (It is converted into em; one character is about + 0.5em on average; digits are exactly 0.5em.) If None, will be calculated. + Default None. + :param config.equal: if True, set all column widths config.equal. Default False. Maybe + ignored, depending on computed ideal column widths. + :param config.caption_align: for the caption + :param config.large_ok: signal that you are intentionally applying to a large + dataframe. Sub-classes may restrict or apply .head() to df. + :param config.max_str_length: maximum displayed length of object types, that + are cast to strings. Eg if you have nested DataFrames! + :param str_table_fmt: table border format used for string output + (markdown), default mixed_grid DEPRECATED?? + :param config.table_width_mode: + 'explicit': set using config.max_table_width_em + 'natural': each cell on one line (can be very wide with long strings) + 'breakable': wrap breakable cells (text strings) at word boundaries + to fit longest word + 'minimum': wrap breakable and ok-to-break (dates) cells + :param config.table_width_header_adjust: additional proportion of table width + used to balance header columns. + :param config.table_width_header_relax: extra spaces allowed per column heading + to facilitate better column header wrapping. + :param config.max_table_width_em: max table width used for markdown string output, + default 200; width is never less than minimum width. Padding (3 chars + per row plus 1) consumed out of config.max_table_width_em in string output mode. + :param config.debug: if True, add id to caption and use colored lines in table, + default False. + """ if config and config_path: raise ValueError( "Pass either 'config' or 'config_path', not both.") @@ -400,14 +400,12 @@ class GT(object): if not np.any((is_integer_dtype(self.df.iloc[:, i]), is_datetime64_any_dtype(self.df.iloc[:, i]))): try: - self.df.iloc[:, i] = self.df.iloc[:, - i].astype(float) + self.df.iloc[:, i] = self.df.iloc[: ,i].astype(float) logger.debug( - f'coerce {i}={c} from {old_type} to float') + f'coerced column {i}={c} from {old_type} to float success') except (ValueError, TypeError): logger.debug( f'coercing {i}={c} from {old_type} to float FAILED') - # massage unbreakable if unbreakable is None: unbreakable = [] @@ -663,7 +661,7 @@ class GT(object): """Year formatter.""" try: return f'{int(x):d}' - except ValueError: + except (TypeError, ValueError): return str(x) def default_raw_formatter(self, x): @@ -862,8 +860,9 @@ class GT(object): def apply_formatters_work(df, formatters): """Apply formatters to a DataFrame.""" try: + # very surprising bug: if df is empty new_df will have type float!! new_df = pd.DataFrame({i: map(f, df.iloc[:, i]) - for i, f in enumerate(formatters)}) + for i, f in enumerate(formatters)}, dtype=object) except TypeError: print('NASTY TYPE ERROR') raise @@ -926,7 +925,11 @@ class GT(object): # and all(self.df_tex == self.df_html)): # self._tex_knowledge_df = self.html_knowledge_df # else: - self._tex_knowledge_df = self.estimate_column_widths_by_mode('tex') + if not self.config.tikz: + # just repeat html so you have something + self._tex_knowledge_df = self.html_knowledge_df + else: + self._tex_knowledge_df = self.estimate_column_widths_by_mode('tex') return self._tex_knowledge_df @property @@ -1040,6 +1043,7 @@ class GT(object): minimum_width = {} header_natural = {} header_minimum = {} + for col_name in df.columns: minimum_width[col_name] = ( df[col_name].str @@ -1128,8 +1132,8 @@ class GT(object): ans['recommended'] = ans['minimum_width'] space = target_width - minimum logger.warning( - 'Desired width too small for pleasant formatting, table will be too wide by spare space %s < 0.', - space) + 'Mode %s, desired width too small for pleasant formatting, table will be too wide by spare space %s em < 0.', + mode, space) logger.info(f'{mode=} {target_width=}, {natural=}, {acceptable=}, {minimum=}, {max_extra=}, {space=}') # this section tweaks the widths for column headers -> text output only. @@ -1609,6 +1613,8 @@ class GT(object): :param label: :return: """ + if not self.config.tikz: + return '' # pull out arguments (convert to local vars - these used to be arguments) column_sep = self.config.tikz_column_sep row_sep = self.config.tikz_row_sep @@ -1792,7 +1798,8 @@ class GT(object): # c = wfloat_format(c) s = f'{nl} {{cell:{ad2[al]}{colw[c]}s}} ' nl = '\\&' - sio.write(s.format(cell=c + '\\I')) + # cols may not be strings... + sio.write(s.format(cell=str(c) + '\\I')) sio.write('\\& \\\\\n') # write table entries diff --git a/greater_tables/fabrications.py b/greater_tables/fabrications.py index 8089d9a..57351df 100644 --- a/greater_tables/fabrications.py +++ b/greater_tables/fabrications.py @@ -9,7 +9,7 @@ from itertools import cycle, chain, count, zip_longest, product, islice import logging from math import prod from pathlib import Path -from typing import Optional, Union +from typing import Optional, Union, Literal import hashlib import random import re @@ -18,17 +18,80 @@ import numpy as np import pandas as pd +__all__ = ['Fabricator', 'make_df', 'quick_df', 'quick_fab', 'rand_df'] logger = logging.getLogger(__name__) +# helpers: random date times +def random_datetime_series( + n: int, + *, + rng, + now: pd.Timestamp | None = None, +) -> pd.Series: + """ + Generate a pandas Series of random datetimes with millisecond precision. + + Algorithm: + 1) Randomly choose units ∈ {days, months, years}. + 2) Draw K ~ Uniform{0, 1, ..., 20} (integer). + 3) Set start = now - K·units. + 4) Sample n datetimes uniformly between [start, now]. + + Parameters + ---------- + n + Number of random datetimes to generate. + seed + RNG seed for reproducibility. + now + Reference end timestamp; defaults to current UTC time. + + Returns + ------- + pandas.Series + Random datetimes (datetime64[ns]) with ms precision. + """ + rng = rng or np.random.default_rng() + now_ts = pd.Timestamp.utcnow() if now is None else pd.Timestamp(now) + + # 1) Units + unit: Literal["days", "months", "years"] = rng.choice( + ["days", "months", "years"]) + + # 2) Integer K for length of period + default_spans = {"days": 730, "months": 120, "years": 16} + k = int(rng.integers(0, default_spans[unit])) + + # 3) Start + if unit == "days": + start = now_ts - pd.DateOffset(days=k) + elif unit == "months": + start = now_ts - pd.DateOffset(months=k) + else: + start = now_ts - pd.DateOffset(years=k) + + # 4) Sample uniformly in nanoseconds + start_ns = start.value + now_ns = now_ts.value + u = rng.random(n) + ns = start_ns + (now_ns - start_ns) * u + stamps = pd.to_datetime(ns.astype("int64")) + + return pd.Series(stamps, name="datetime") + + +# main class class Fabricator: """ Fabricate dataframes. """ - metric_roots = ['absorption', 'acceleration', 'account', 'activation', 'adjustment', 'allocation', 'amplitude', 'approval', 'asset', 'atom', 'attrition', 'balance', 'band', 'binding', 'cancellation', 'capacitance', 'capital', 'cashflow', 'category', 'cell', 'charge', 'claim', 'commission', 'compound', 'concentration', 'conductivity', 'constraint', 'consumption', 'conversion', 'correlation', 'cost', 'count', 'coverage', 'credit', 'current', 'debt', 'decay', 'decibel', 'deductible', 'deficit', 'deflator', 'demand', 'density', 'development', 'diffusion', 'discount', 'distribution', 'dividend', 'dose', 'duration', 'earnings', 'efficiency', 'elasticity', 'employment', 'energy', 'entropy', 'enzyme', 'estimate', 'excess', 'exhaustion', 'expense', 'exposure', 'failure', 'field', 'flux', 'force', 'frequency', 'funding', 'gdp', 'gene', 'gradient', 'growth', 'half_life', 'incidence', 'income', 'index', 'indicator', 'inequality', 'inflation', 'inhibition', 'input', 'intensity', 'investment', 'kurtosis', 'lapse', 'layer', 'leverage', 'liability', 'limit', 'loss', 'luminosity', 'margin', 'mass', 'molecule', 'momentum', 'mortality', 'neutron', 'noise', 'operating', 'output', 'penalty', 'photon', 'policy', 'portfolio', 'potential', 'power', 'preference', 'premium', 'pressure', 'price', 'productivity', 'profit', 'protein', 'proton', 'provision', 'radiation', 'rate', 'ratio', 'reaction', 'recovery', 'reflection', 'refraction', 'renewal', 'reserve', 'residual', 'resistance', 'return', 'revenue', 'risk', 'sample', 'savings', 'scenario', 'score', 'sector', 'settlement', 'severity', 'shock', 'shortfall', 'signal', 'skewness', 'spread', 'strain', 'stress', 'subsidy', 'supply', 'tail', 'tariff', 'tax', 'temperature', 'tension', 'term', 'threshold', 'trade', 'trend', 'turbulence', 'unemployment', 'uptake', 'utility', 'utilization', 'valuation', 'variance', 'velocity', 'viscosity', 'volatility', 'voltage', 'volume', 'wage', 'wavelength', 'wealth', 'weight', 'yield'] + metric_roots = ['absorption', 'acceleration', 'account', 'activation', 'adjustment', 'allocation', 'amplitude', 'approval', 'asset', 'atom', 'attrition', 'balance', 'band', 'binding', 'cancellation', 'capacitance', 'capital', 'cashflow', 'category', 'cell', 'charge', 'claim', 'commission', 'compound', 'concentration', 'conductivity', 'constraint', 'consumption', 'conversion', 'correlation', 'cost', 'count', 'coverage', 'credit', 'current', 'debt', 'decay', 'decibel', 'deductible', 'deficit', 'deflator', 'demand', 'density', 'development', 'diffusion', 'discount', 'distribution', 'dividend', 'dose', 'duration', 'earnings', 'efficiency', 'elasticity', 'employment', 'energy', 'entropy', 'enzyme', 'estimate', 'excess', 'exhaustion', 'expense', 'exposure', 'failure', 'field', 'flux', 'force', 'frequency', 'funding', 'gdp', 'gene', 'gradient', 'growth', 'half_life', 'incidence', 'income', 'index', 'indicator', 'inequality', 'inflation', 'inhibition', 'input', 'intensity', 'investment', + 'kurtosis', 'lapse', 'layer', 'leverage', 'liability', 'limit', 'loss', 'luminosity', 'margin', 'mass', 'molecule', 'momentum', 'mortality', 'neutron', 'noise', 'operating', 'output', 'penalty', 'photon', 'policy', 'portfolio', 'potential', 'power', 'preference', 'premium', 'pressure', 'price', 'productivity', 'profit', 'protein', 'proton', 'provision', 'radiation', 'rate', 'ratio', 'reaction', 'recovery', 'reflection', 'refraction', 'renewal', 'reserve', 'residual', 'resistance', 'return', 'revenue', 'risk', 'sample', 'savings', 'scenario', 'score', 'sector', 'settlement', 'severity', 'shock', 'shortfall', 'signal', 'skewness', 'spread', 'strain', 'stress', 'subsidy', 'supply', 'tail', 'tariff', 'tax', 'temperature', 'tension', 'term', 'threshold', 'trade', 'trend', 'turbulence', 'unemployment', 'uptake', 'utility', 'utilization', 'valuation', 'variance', 'velocity', 'viscosity', 'volatility', 'voltage', 'volume', 'wage', 'wavelength', 'wealth', 'weight', 'yield'] - metric_suffix = ["", "rate", "score", "amount", "index", "ratio", "factor", "value"] + metric_suffix = ["", "rate", "score", "amount", + "index", "ratio", "factor", "value"] def __init__(self, decorate=False, seed: Optional[int] = None): """ @@ -66,7 +129,7 @@ class Fabricator: # trim down slightly # dont' want | in tex...messes up tables! pat = re.compile(r'(? 1: - df = self.uber(oversample * rows, metrics, data_spec, index_levels=index_levels, - - index_names=index_names, column_groups=column_groups, column_levels=column_levels, - column_names=column_names, decorate=decorate, oversample=1) + df = self.make(oversample * rows, metrics, data_spec, index_levels=index_levels, + index_names=index_names, column_groups=column_groups, column_levels=column_levels, + column_names=column_names, decorate=decorate, oversample=1) df = df.iloc[:rows, :] return df inames = index_names or [f'i_{i}' for i in range(index_levels)] - index = pd.MultiIndex.from_tuples(islice(product(*(self._generate_column('s', v) for v in self.primes_for_product(rows, index_levels))), rows), names=inames) + index = pd.MultiIndex.from_tuples(islice(product(*(self._generate_column( + 's', v) for v in self.primes_for_product(rows, index_levels))), rows), names=inames) # create with col groups and drop later if needed if metric_name_spec == '': metric_names = [self.metric_name(t) for t in data_spec] + elif isinstance(metric_name_spec, (list, tuple)): + metric_names = [i.strip() for i in metric_name_spec] + assert len(metric_names) == metrics, f"metric_name_spec must have name for each of { + metrics} metrics" else: metric_name_spec = self._parse_colspec(metric_name_spec) - assert len(metric_name_spec) == len(data_spec), "metric name spec not consistent with data spec" - metric_names = [self._generate_column(dt, 1).iloc[0] for dt in metric_name_spec] + assert len(metric_name_spec) == len( + data_spec), "metric name spec not consistent with data spec" + metric_names = [self._generate_column( + dt, 1).iloc[0] for dt in metric_name_spec] if column_groups > 0: - cnames = (column_names or [f'c_{i}' for i in range(column_levels)]) + ['metric'] + cnames = (column_names or [ + f'c_{i}' for i in range(column_levels)]) + ['metric'] columns_pfp = self.primes_for_product(column_groups, column_levels) - cgroup_product = product(*(self._generate_column('s', v) for v in columns_pfp)) + cgroup_product = product( + *(self._generate_column('s', v) for v in columns_pfp)) # take first column_groups entries - islice works without creating the full iterable cgroup_product = islice(cgroup_product, column_groups) # add metrics @@ -214,7 +290,7 @@ class Fabricator: # fill in the data, data_spec x column_groups groups for c, dt in zip(df.columns, data_spec * column_groups): - df[c] =self._generate_column(dt, rows).values + df[c] = self._generate_column(dt, rows).values if simplify: df = self.drop_singleton_levels(df) @@ -254,14 +330,16 @@ class Fabricator: metrics = columns index_levels = self.rng.integers(1, 3 + 1) if rows == 0: - rows = self.rng.integers(5 * (column_groups + index_levels), 10 * (column_groups + index_levels) + 1) + rows = self.rng.integers( + 5 * (column_groups + index_levels), 10 * (column_groups + index_levels) + 1) - valid_types = [i for i in ['f', 's2', 's' 'i', 'l', 'f', 'f', 'd', 'f', 'i', 's3', 'l', 'h', 't', 'p', 'x', 'r', 'y'] if i not in omit] + valid_types = [i for i in ['f', 's2', 's' 'i', 'l', 'f', 'f', 'd', + 'f', 'i', 's3', 'l', 'h', 't', 'p', 'x', 'r', 'y'] if i not in omit] data_spec = ''.join(self.rng.choice(valid_types, size=metrics)) missing = round(float(self.rng.uniform(0, 0.15)), 2) return self.make(rows=rows, data_spec=data_spec, index_levels=index_levels, - column_groups=column_groups, column_levels=column_levels, - decorate=False, simplify=True, oversample=1) + column_groups=column_groups, column_levels=column_levels, + decorate=False, simplify=True, oversample=1) def _parse_colspec(self, spec: str) -> list[str]: return re.findall(r's\d+|[a-z]', spec) @@ -286,9 +364,7 @@ class Fabricator: if dtype == 'i': return pd.Series(self.rng.integers(-1e4, 1e6, size=n), dtype='int64') if dtype == 'd': - start_date = Fabricator.random_date_within_last_n_years( - 10) - return pd.Series(pd.date_range(start=start_date, periods=n, freq='D')) + return random_datetime_series(n, rng=self.rng) if dtype == 'y': return pd.Series(random.sample(range(1990, 2031), n)) if dtype == 't': @@ -312,7 +388,7 @@ class Fabricator: def metric_name(self, type_hint): """Return a one-word metric name.""" - nm = next(self._metric_namer) + nm = next(self._metric_namer) if self.decorate: if type_hint == 'y': nm += ' year' @@ -395,3 +471,12 @@ class Fabricator: if shuffle: self.rng.shuffle(primes) return primes + + +def quick_fab(rows: int = 10, data_spec: str = 's3sfid', **kwargs): + """One-stop quick fabrication of a random dataframe.""" + fab = Fabricator() + return fab.make(rows, data_spec, **kwargs) + + +rand_df = make_df = quick_df = quick_fab diff --git a/greater_tables/utilities.py b/greater_tables/utilities.py index 99e2aa9..73701f7 100644 --- a/greater_tables/utilities.py +++ b/greater_tables/utilities.py @@ -310,6 +310,10 @@ class TextLength: @staticmethod def text_display_len(s: str) -> float: """Estimate display width in ems, ignoring HTML tags, interpreting TeX, and HTML entities.""" + # can be called on an index that may not be a string?? + if not isinstance(s, str): + # print(f'{s} is {type(s)}!!') + s = str(s) def strip_html_tags(text): return re.sub(r'<[^>]*>', '', text) @@ -863,3 +867,25 @@ class RichOutput: table.add_row(*row.tolist()) return table + + +class SmartTitle(): + """Support reasonable Title case for text.""" + # TODO: Implement smart titling! + @staticmethod + def smart_title(text): + """Slightly smart title capitalization (GPT4o).""" + small_words = {"a", "an", "and", "as", "at", "but", "by", "for", + "in", "is", "of", "on", "or", "the", "to", "up", "via", "vs"} + words = text.split() + result = [] + + for i, word in enumerate(words): + if len(word) <= 3 and word.isupper(): + result.append(word) # already acronym-like + elif word.lower() in small_words and i != 0: + result.append(word.lower()) + else: + result.append(word.capitalize()) + + return " ".join(result)