diff --git a/greater_tables/greater_tables.py b/greater_tables/greater_tables.py index cdf7d34..422a104 100644 --- a/greater_tables/greater_tables.py +++ b/greater_tables/greater_tables.py @@ -6,7 +6,7 @@ from itertools import groupby import logging import numpy as np import pandas as pd -from pandas.api.types import is_datetime64_any_dtype, is_integer_dtype,\ +from pandas.api.types import is_datetime64_any_dtype, is_integer_dtype, \ is_float_dtype # , is_numeric_dtype from pathlib import Path import re @@ -34,7 +34,8 @@ 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') +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.') @@ -143,10 +144,12 @@ class GT(object): df, aligners = GT.md_to_df(df) show_index = False else: - raise ValueError('df must be a DataFrame, a list of lists, or a markdown table string') + raise ValueError( + 'df must be a DataFrame, a list of lists, or a markdown table string') if len(df) > 50 and not large_ok: - raise ValueError('Large dataframe (>50 rows) and large_ok not set to true...do you know what you are doing?') + raise ValueError( + 'Large dataframe (>50 rows) and large_ok not set to true...do you know what you are doing?') if not df.columns.is_unique: raise ValueError('df column names are not unique') @@ -157,7 +160,8 @@ class GT(object): # self.df.columns.names = [None] * self.df.columns.nlevels self.df_id = df_short_hash(self.df) self.debug = debug - self.caption = caption + (' (id: ' + self.df_id + ')' if self.debug else '') + self.caption = caption + \ + (' (id: ' + self.df_id + ')' if self.debug else '') self.max_str_length = max_str_length # before messing self.show_index = show_index @@ -168,20 +172,25 @@ class GT(object): with warnings.catch_warnings(): if self.show_index: - warnings.simplefilter("ignore", category=pd.errors.PerformanceWarning) - self.df = self.df.reset_index(drop=False, col_level=self.df.columns.nlevels - 1) + warnings.simplefilter( + "ignore", category=pd.errors.PerformanceWarning) + self.df = self.df.reset_index( + drop=False, col_level=self.df.columns.nlevels - 1) # want the new index to be ints - that is not default if old was multiindex self.df.index = np.arange(self.df.shape[0], dtype=int) - self.index_change_level = GT.changed_column(self.df.iloc[:, :self.nindex]) + self.index_change_level = GT.changed_column( + self.df.iloc[:, :self.nindex]) if self.ncolumns > 1: # will be empty rows above the index headers - self.index_change_level = pd.Series([i[-1] for i in self.index_change_level]) + self.index_change_level = pd.Series( + [i[-1] for i in self.index_change_level]) self.column_change_level = GT.changed_level(self.raw_df.columns) # determine ratio columns if ratio_cols is not None and np.any(self.df.columns.duplicated()): - logger.warning('Ratio cols specified with non-unique column names: ignoring request.') + logger.warning( + 'Ratio cols specified with non-unique column names: ignoring request.') self.ratio_cols = [] else: if ratio_cols is None: @@ -195,7 +204,8 @@ class GT(object): # determine year columns if year_cols is not None and np.any(self.df.columns.duplicated()): - logger.warning('Year cols specified with non-unique column names: ignoring request.') + logger.warning( + 'Year cols specified with non-unique column names: ignoring request.') self.year_cols = [] else: if year_cols is None: @@ -206,17 +216,21 @@ class GT(object): self.year_cols = year_cols with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=pd.errors.PerformanceWarning) + warnings.simplefilter( + "ignore", category=pd.errors.PerformanceWarning) if cast_to_floats: for i, c in enumerate(self.df.columns): old_type = self.df.dtypes[c] 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) - logger.debug(f'coerce {i}={c} from {old_type} to float') + self.df.iloc[:, i] = self.df.iloc[:, + i].astype(float) + logger.debug( + f'coerce {i}={c} from {old_type} to float') except (ValueError, TypeError): - logger.debug(f'coercing {i}={c} from {old_type} to float FAILED') + logger.debug( + f'coercing {i}={c} from {old_type} to float FAILED') # now can determine types self.float_col_indices = [] @@ -242,7 +256,8 @@ class GT(object): # figure out column and index alignment if aligners is not None and np.any(self.df.columns.duplicated()): - logger.warning('aligners specified with non-unique column names: ignoring request.') + logger.warning( + 'aligners specified with non-unique column names: ignoring request.') aligners = None if aligners is None: # not using @@ -288,12 +303,15 @@ class GT(object): fmt = f'{{x:.{v}f}}' self.default_formatters[k] = lambda x: fmt.format(x=x) else: - raise ValueError('formatters must be dict of callables or ints or format strings {x:...}') + raise ValueError( + 'formatters must be dict of callables or ints or format strings {x:...}') # store defaults self.default_integer_str = default_integer_str - self.default_float_str = default_float_str # VERY rarely used; for floats in cols that are not floats - self.default_date_str = default_date_str.replace('{x:', '').replace('}', '') + # VERY rarely used; for floats in cols that are not floats + self.default_float_str = default_float_str + self.default_date_str = default_date_str.replace( + '{x:', '').replace('}', '') self.default_ratio_str = default_ratio_str self.pef_precision = pef_precision self.pef_lower = pef_lower @@ -329,11 +347,13 @@ class GT(object): elif spacing == 'wide': padding_trbl = (4, 15, 4, 15) else: - raise ValueError('spacing must be tight, medium, or wide or tuple of four ints.') + raise ValueError( + 'spacing must be tight, medium, or wide or tuple of four ints.') try: self.padt, self.padr, self.padb, self.padl = padding_trbl except ValueError: - logger.error(f'padding_trbl {padding_trbl=}, must be four ints, defaultign to medium') + logger.error( + f'padding_trbl {padding_trbl=}, must be four ints, defaultign to medium') self.padt, self.padr, self.padb, self.padl = 2, 10, 2, 10 # because of the problem of non-unique indexes use a list and @@ -386,26 +406,50 @@ class GT(object): except ValueError: return str(x) + # def default_formatter(self, x): + # """Universal formatter for other types.""" + # try: + # # werid wrinkle here: float('infinity') -> np.inf!! + # f = float(x) + # if self.default_float_formatter: + # return self.default_float_formatter(f) + # try: + # i = int(x) + # except ValueError: + # try: + # i = int(f) + # except OverflowError: + # # this came up! Passed the work "Infinity" + # return str(x) + # if i == f: + # return self.default_integer_str.format(x=i) + # else: + # # TODO BEEF UP? + # return self.default_float_str.format(x=f) + # except (TypeError, ValueError): + # if self.max_str_length < 0: + # return str(x) + # else: + # return str(x)[:self.max_str_length] + def default_formatter(self, x): - """Universal formatter for other types.""" + """Universal formatter for other types (GTP re-write of above cluster.""" try: f = float(x) - if self.default_float_formatter: - return self.default_float_formatter(f) - try: - i = int(x) - except ValueError: - i = int(f) - if i == f: - return self.default_integer_str.format(x=i) - else: - # TODO BEEF UP? - return self.default_float_str.format(x=f) except (TypeError, ValueError): - if self.max_str_length < 0: - return str(x) - else: - return str(x)[:self.max_str_length] + s = str(x) + return s if self.max_str_length < 0 else s[:self.max_str_length] + + if self.default_float_formatter: + return self.default_float_formatter(f) + + if np.isinf(f) or np.isnan(f): # clearer handling of weird float cases + return str(x) + + if f.is_integer(): + return self.default_integer_str.format(x=int(f)) + else: + return self.default_float_str.format(x=f) def pef(self, x): """Pandas engineering format.""" @@ -488,8 +532,10 @@ class GT(object): self.default_float_formatter = ff else: if type(self.table_float_format) != str: - raise ValueError('table_float_format must be a string or a function') + raise ValueError( + 'table_float_format must be a string or a function') fmt = self.table_float_format + def ff(x): try: return fmt.format(x=x) @@ -520,13 +566,15 @@ class GT(object): self._df_formatters.append(self.default_integer_formatter) elif i in self.float_col_indices: # trickier approach... - self._df_formatters.append(self.default_float_formatter or self.make_float_formatter(self.df.iloc[:, i])) + self._df_formatters.append( + self.default_float_formatter or self.make_float_formatter(self.df.iloc[:, i])) else: # print(f'{i} default') self._df_formatters.append(self.default_formatter) # self._df_formatters is now a list of length equal to cols in df if len(self._df_formatters) != self.df.shape[1]: - raise ValueError(f'Something wrong: {len(self._df_formatters)=} != {self.df.shape=}') + raise ValueError( + f'Something wrong: {len(self._df_formatters)=} != {self.df.shape=}') return self._df_formatters def __repr__(self): @@ -577,9 +625,9 @@ class GT(object): font-family: "Roboto", "Open Sans Condensed", "Arial", 'Segoe UI', sans-serif; font-size: {self.font_body}em; width: auto; - /* tb and lr + /* tb and lr width: fit-content; */ - margin: 10px auto; + margin: 10px auto; border: none; overflow: auto; margin-left: auto; @@ -668,7 +716,8 @@ class GT(object): def make_html(self): """Convert a pandas DataFrame to an HTML table.""" - index_name_to_level = dict(zip(self.raw_df.index.names, range(self.nindex))) + index_name_to_level = dict( + zip(self.raw_df.index.names, range(self.nindex))) index_change_level = self.index_change_level.map(index_name_to_level) # this is easier and computed in the init column_change_level = self.column_change_level @@ -683,7 +732,8 @@ class GT(object): idx_header = bit.iloc[:self.nindex, :self.ncolumns] columns = bit.iloc[self.nindex:, :self.ncolumns] - colw, tabs = GT.estimate_column_widths(self.df, nc_index=self.nindex, scale=1, equal=self.equal) + colw, tabs = GT.estimate_column_widths( + self.df, nc_index=self.nindex, scale=1, equal=self.equal) if self.debug: print(f'Input {self.tabs=}\nComputed {tabs=}') if self.tabs is not None: @@ -692,7 +742,8 @@ class GT(object): elif len(self.tabs) == 1: tabs = self.tabs * len(tabs) else: - logger.error(f'{self.tabs=} must be None, a single number, or a list of numbers of the correct length. Ignoring.') + logger.error( + 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 @@ -703,7 +754,8 @@ class GT(object): # add the padding # TODO FONT SIZE # /4 works well for the tests (handles dates) but seems a bit illogical... - tabs = np.array(tabs) + (self.padl + self.padr) / 12 # guessing font size... + # guessing font size... + tabs = np.array(tabs) + (self.padl + self.padr) / 12 # em_per_char = 0.5; true exactly for tabular-nums em_per_char = 0.6 tabs = tabs * em_per_char @@ -740,7 +792,7 @@ class GT(object): # need :i+1 to get down to the ith level cum_col = 0 # keep track of where we are up to for j, (nm, g) in enumerate(groupby(columns.iloc[:, :i+1]. - apply(lambda x: ':::'.join(str(i) for i in x), axis=1))): + apply(lambda x: ':::'.join(str(i) for i in x), axis=1))): # ::: needs to be something that does not appear in the col names # need to combine for groupby but be able to split off the last level # picks off the name of the bottom level @@ -756,9 +808,11 @@ class GT(object): vrule = '' if j == 0 and not self.show_index: # first column, no index, left align label - html.append(f'