mirror of
https://github.com/wassname/greater_tables_project.git
synced 2026-07-25 13:10:12 +08:00
Solid draft
Issues with spancols in some cases... Moved new html code to prod Rationalized test_df creation
This commit is contained in:
+469
-662
File diff suppressed because it is too large
Load Diff
+194
-510
@@ -7,15 +7,9 @@ from pathlib import Path
|
||||
import random
|
||||
import sys
|
||||
from IPython.display import HTML, display
|
||||
from docs.conf import html_theme
|
||||
|
||||
from . greater_tables import GT
|
||||
|
||||
# Load a list of words
|
||||
p = Path('C:\\s\\Websites\\new_mynl\\word_lists\\match 12.md')
|
||||
word_list = p.read_text().split('\n')
|
||||
|
||||
|
||||
# GPT recommended approach
|
||||
logger = logging.getLogger(__name__)
|
||||
# Disable log propagation to prevent duplicates
|
||||
@@ -34,142 +28,9 @@ logger.addHandler(handler)
|
||||
logger.info('Logger Setup; module recompiled.')
|
||||
|
||||
|
||||
# __gt_global = GT()
|
||||
|
||||
|
||||
# def qhtml(df, **kwargs):
|
||||
# """Generic "quick display" function."""
|
||||
# return HTML(__gt_global(df, **kwargs))
|
||||
|
||||
|
||||
# def qd(df, **kwargs):
|
||||
# """Generic "quick display" function."""
|
||||
# if isinstance(df, pd.Series):
|
||||
# if df.name is None:
|
||||
# df.name = 'value'
|
||||
# df = df.to_frame()
|
||||
# return display(HTML(__gt_global(df, **kwargs)))
|
||||
|
||||
|
||||
def create_three_level_multiindex(df):
|
||||
"""
|
||||
Adds two random levels to a DataFrame's column MultiIndex.
|
||||
|
||||
Parameters:
|
||||
df (pd.DataFrame): Input DataFrame.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: DataFrame with a three-level MultiIndex on the columns.
|
||||
"""
|
||||
n_columns = len(df.columns)
|
||||
level_1 = np.random.choice(["A", "B", "C"], size=n_columns)
|
||||
level_2 = np.random.choice(["X", "Y", "Z"], size=n_columns)
|
||||
|
||||
# Create the MultiIndex
|
||||
multi_index = pd.MultiIndex.from_tuples(
|
||||
[(l1, l2, col) for l1, l2, col in zip(level_1, level_2, df.columns)],
|
||||
names=["Level 1", "Level 2", df.columns.name]
|
||||
)
|
||||
|
||||
# Apply the new MultiIndex to the DataFrame
|
||||
df.columns = multi_index
|
||||
return df
|
||||
|
||||
|
||||
def test_df(date=False, mi_columns=True):
|
||||
"""Make a test dataframe nr rows with multi index."""
|
||||
nr = 10
|
||||
words = 'Parliament organised a year-long programme of events called Parliament in the Making to celebrate the 800th anniversary of the sealing of Magna Carta on 15 June and the 750th anniversary of the first representative parliament on 20 January Events were coordinated with Parliament Week'
|
||||
words = list(set(words.split(' ')))
|
||||
w1 = ['Abel', 'Cain', 'Issac', 'Fred', 'George', 'Harry', 'Ivan', 'John', 'Karl', 'Lenny', 'Moe', 'Ned', 'Otto', 'Paul', 'Quinn', 'Ralph', 'Steve', 'Tom', 'Ulysses', 'Victor', 'Walter', 'Xavier', 'Yuri', 'Zach']
|
||||
w2 = ['South', 'East', 'West', 'North', "North West", "North East", "South West", "South East", "Central", "Outer", "Inner", "Mid", "Upper", "Lower", "Far", "Near", "Middle", "Farthest", "Nearest"]
|
||||
w4 = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG', 'HH', 'II', 'JJ', 'KK', 'LL', 'MM', 'NN', 'OO', 'PP', 'QQ', 'RR', 'SS', 'TT', 'UU', 'VV', 'WW', 'XX', 'YY', 'ZZ']
|
||||
rints = [0, -10, 10, 100, -100, 1000, -1000, 10000, -10000, 100000, -100000, 1000000, -1000000]
|
||||
df = pd.DataFrame({'idx1': np.random.choice(w1, nr),
|
||||
'idx2': np.random.choice(w2, nr),
|
||||
'idx3': np.random.poisson(2, nr),
|
||||
'floats': np.random.rand(nr) * 3000.,
|
||||
'smaller': np.random.rand(nr) * 10 ** np.linspace(-3, 4, nr),
|
||||
'larger': np.random.choice([-1., 0, 1.], nr) * np.random.rand(nr) * 10 ** np.linspace(3, 12, nr),
|
||||
'ints': np.random.poisson(20, nr),
|
||||
'powers': np.pi * 10. ** np.arange(-20, 26, 5),
|
||||
'ratios': np.random.rand(nr) * 3. - 1.,
|
||||
'string': [' '.join(np.random.choice(words, 4, replace=False)) for i in range(nr)],
|
||||
# 'object': [np.random.poisson(2, nr) for i in range(nr)]
|
||||
})
|
||||
|
||||
if date:
|
||||
df['date'] = [dt.datetime.fromordinal(np.random.randint(dt.date(2020, 1, 1).toordinal(),
|
||||
dt.date(2030, 1, 1).toordinal())) + dt.timedelta(seconds=np.random.randint(86400)) for _ in range(nr)]
|
||||
df['date'] = pd.to_datetime(df['date'])
|
||||
df.columns.name = 'Col name'
|
||||
df = df.set_index(['idx1', 'idx2', 'idx3'])
|
||||
if mi_columns:
|
||||
df = create_three_level_multiindex(df)
|
||||
|
||||
# check unique and sort
|
||||
df = df.loc[df.index[~df.index.duplicated()]]
|
||||
assert np.all(~df.columns.duplicated()), 'Columns not all unique'
|
||||
df = df.sort_index(axis=0).sort_index(axis=1)
|
||||
return df
|
||||
|
||||
|
||||
def make_test_dfs():
|
||||
"""Make a dict of test dataframes with different characteristics."""
|
||||
ans = {}
|
||||
df = pd.DataFrame({'x': [int(i) for i in 3. ** np.arange(10)], 'y': np.arange(10, dtype=float)})
|
||||
df['w'] = df.x ** 0.35
|
||||
df['z'] = df.y ** .25
|
||||
|
||||
ans['basic'] = df.copy()
|
||||
df1 = df.copy()
|
||||
df1.index.name = 'idx name'
|
||||
ans['basic w idx name'] = df1.copy()
|
||||
|
||||
df1 = df.copy()
|
||||
df1.columns.name = 'col name'
|
||||
ans['basic w col name'] = df1.copy()
|
||||
|
||||
df1 = df.copy()
|
||||
df1.index.name = 'idx name'
|
||||
df1.columns.name = 'col name'
|
||||
ans['basic w both names'] = df1.copy()
|
||||
|
||||
df1['date'] = [dt.datetime.fromordinal(np.random.randint(dt.date(2020, 1, 1).toordinal(),
|
||||
dt.date(2030, 1, 1).toordinal())) + dt.timedelta(seconds=np.random.randint(86400))
|
||||
for _ in range(len(df1))]
|
||||
df2 = df1.set_index('date')
|
||||
ans['time series'] = df2.copy()
|
||||
|
||||
df2 = df1.set_index('date', append=True)
|
||||
ans['time series and range'] = df2.copy()
|
||||
|
||||
valid = False
|
||||
while not valid:
|
||||
a = ans['realistic'] = test_df(date=False, mi_columns=False)
|
||||
valid = a.index.is_unique and a.columns.is_unique
|
||||
|
||||
valid = False
|
||||
while not valid:
|
||||
a = ans['realistic w date'] = test_df(date=True, mi_columns=False).droplevel(2, axis=0)
|
||||
valid = a.index.is_unique and a.columns.is_unique
|
||||
|
||||
valid = False
|
||||
while not valid:
|
||||
a = ans['realistic mi'] = test_df(date=False, mi_columns=True).droplevel(2, axis=1)
|
||||
valid = a.index.is_unique and a.columns.is_unique
|
||||
|
||||
valid = False
|
||||
while not valid:
|
||||
a = ans['realistic mi w date'] = test_df(date=True, mi_columns=True).droplevel(2, axis=0).droplevel(2, axis=1)
|
||||
valid = a.index.is_unique and a.columns.is_unique
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
|
||||
header = '''
|
||||
---
|
||||
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:
|
||||
@@ -187,6 +48,7 @@ format:
|
||||
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
|
||||
|
||||
@@ -194,174 +56,112 @@ from IPython.display import display
|
||||
|
||||
...code build completed.
|
||||
|
||||
# Greater_tables
|
||||
|
||||
# Greater_tables Output
|
||||
|
||||
```{{python}}
|
||||
#| echo: true
|
||||
#| label: greater-tables-test
|
||||
ans = gter.make_test_dfs()
|
||||
test_gen = gtu.TestDFGenerator()
|
||||
ans = test_gen.test_suite()
|
||||
```
|
||||
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def write_all_tables():
|
||||
"""Write a tester for all tables to a qmd file."""
|
||||
global header
|
||||
|
||||
template = '''
|
||||
|
||||
## Test Table {k}
|
||||
|
||||
```{{python}}
|
||||
#| echo: fold
|
||||
#| label: tbl-greater-tables-test-{i}
|
||||
#| tbl-cap: Output for test table {k}
|
||||
hrw = {hrw}
|
||||
gter.GT(ans['{k}'], "{title}", ratio_cols='z', aligners={{'w': 'l'}},
|
||||
f = gter.GT(ans['{k}'], "{title}", ratio_cols='z', aligners={{'w': 'l'}},
|
||||
hrule_widths=hrw)
|
||||
h = f._repr_html_()
|
||||
print(f.df.dtypes)
|
||||
h
|
||||
```
|
||||
|
||||
SPACER
|
||||
Comments go here.
|
||||
|
||||
'''
|
||||
ans = make_test_dfs()
|
||||
out = [header.format(title='All Tables Test')]
|
||||
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, 0.5, 0)
|
||||
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('\\s\\telos\\pmir_studynote\\quarto_scratch\\tables.qmd')
|
||||
p.write_text('\n'.join(out), encoding='utf-8')
|
||||
|
||||
|
||||
def apply_css(f, idx):
|
||||
"""Apply subset of css to the GT object f"""
|
||||
style = f.df_style
|
||||
split_style = style.split(f'#{f.df_id}')
|
||||
print(f'style has {len(split_style)} separate entries')
|
||||
n = 2000
|
||||
if idx == 0:
|
||||
print('RAW')
|
||||
display(HTML(f.df.to_html(formatters=f.df_formatters)))
|
||||
print('='*80)
|
||||
if not isinstance(idx, (tuple, list)):
|
||||
idx = [idx]
|
||||
newbit = []
|
||||
for i in idx:
|
||||
code = f'T{n+i}'
|
||||
h = f.df_html.replace(f.df_id, code)
|
||||
bit = split_style[i].strip()
|
||||
if i == 1:
|
||||
newbit.append(f'#{code} {split_style[1]}\n')
|
||||
else:
|
||||
newbit.append(f'#{code} {bit}')
|
||||
newbit = '<style>\n' + '\n'.join(newbit) + '\n</style>\n'
|
||||
print(newbit)
|
||||
newcode = f'{newbit}{h}'
|
||||
display(HTML(newcode))
|
||||
|
||||
|
||||
def incremental_qmd(nm, css_bits=22):
|
||||
"""Write a qmd file with incremental tables."""
|
||||
global header
|
||||
|
||||
bit = '''
|
||||
|
||||
```{{python}}
|
||||
#| echo: fold
|
||||
#| label: tbl-greater-tables-test-{i}
|
||||
gter.apply_css(f, {i})
|
||||
```
|
||||
'''
|
||||
step1 = f'''
|
||||
|
||||
## GT Format
|
||||
|
||||
```{{python}}
|
||||
#| echo: fold
|
||||
#| label: setup-01
|
||||
df = ans['{nm}']
|
||||
f = gter.GT(df, "{nm}", ratio_cols='z', aligners={{'w': 'l'}},
|
||||
hrule_widths=(1.5, 1, 0.5), vrule_widths=(1.5, 1, 0.5))
|
||||
f
|
||||
```
|
||||
|
||||
# Incremental Test Suite
|
||||
|
||||
'''
|
||||
out = [header.format(title='Incremental Build Test'), step1]
|
||||
|
||||
for i in range(css_bits):
|
||||
out.append(bit.format(i=i))
|
||||
p = Path('\\s\\telos\\pmir_studynote\\quarto_scratch\\seq_build.qmd')
|
||||
p = Path(out_path)
|
||||
p.write_text('\n'.join(out), encoding='utf-8')
|
||||
|
||||
|
||||
# ==================================================
|
||||
# SUPER DOOPER test df generator with help from GPT
|
||||
def make_column_names(n, g, words):
|
||||
"""Make n column names each g words long."""
|
||||
return [' '.join(x).title() for x in zip(*[iter(words[:n*g])] * g)]
|
||||
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 = None
|
||||
|
||||
def __init__(self, nan_proportion=0.05, missing_proportion=0,
|
||||
title=False, sep='_', file_path=None):
|
||||
"""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
|
||||
file_path = file_path or TestDFGenerator._word_list_path
|
||||
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 generate_test_dataframe(
|
||||
num_rows=10,
|
||||
num_columns=5,
|
||||
num_index_levels=1,
|
||||
num_column_levels=1,
|
||||
column_name_length=3,
|
||||
dtype_label=True,
|
||||
nan_proportion=0.05,
|
||||
missing_proportion=0.05,
|
||||
index_types=None,
|
||||
words=None
|
||||
):
|
||||
"""
|
||||
Generate a random pandas DataFrame with diverse structures for testing.
|
||||
def __repr__(self):
|
||||
"""Return a string representation."""
|
||||
return f"TestDFGenerator({len(self.words):,d} words)"
|
||||
|
||||
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.
|
||||
- nan_proportion (float): Proportion of NaNs.
|
||||
- missing_proportion (float): Proportion of None values.
|
||||
- index_types (list): List of index data types for each level.
|
||||
- words (list): List of words for generating column names.
|
||||
@staticmethod
|
||||
def load_words(file_path):
|
||||
"""Load a list of words from a file."""
|
||||
p = Path(file_path)
|
||||
txt = p.read_text(encoding='utf-8')
|
||||
wl = txt.split('\n')
|
||||
logger.info(f"Loaded wordlist.") # Debug print
|
||||
return wl
|
||||
|
||||
Returns:
|
||||
- pd.DataFrame: A test DataFrame with diverse structures.
|
||||
"""
|
||||
global word_list # Ensure access to global words
|
||||
@property
|
||||
def words(self):
|
||||
"""Return the word list."""
|
||||
random.shuffle(self._word_list)
|
||||
return self._word_list
|
||||
|
||||
# Use default word list if not provided
|
||||
if words is None or len(words) < num_columns:
|
||||
words = word_list
|
||||
random.shuffle(words)
|
||||
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)]
|
||||
|
||||
# Generate column names
|
||||
col_names = make_column_names(num_columns, max(1, column_name_length - (1 if dtype_label else 0)), words)
|
||||
|
||||
# Randomly select index data types for each level
|
||||
index_dtypes = ["int", "float", "str", "date", "datetime"]
|
||||
index_probs = np.array([20, 1, 20, 5, 5], dtype=float)
|
||||
index_probs /= index_probs.sum()
|
||||
if index_types is None:
|
||||
index_types = np.random.choice(index_dtypes, num_index_levels, p=index_probs, replace=True)
|
||||
if len(index_types) < num_index_levels:
|
||||
# well...
|
||||
index_types = (index_types * 10)[:num_index_levels]
|
||||
|
||||
def generate_index_data(dtype, size):
|
||||
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(words, size=size)
|
||||
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)]
|
||||
@@ -373,67 +173,46 @@ def generate_test_dataframe(
|
||||
seconds=random.randint(0, 59),
|
||||
microseconds=random.randint(0, 999999))
|
||||
for _ in range(size)]
|
||||
return values
|
||||
return values # noqa
|
||||
|
||||
def generate_multi_index(dtypes, levels, num_rows):
|
||||
def make_multi_index(self, dtypes, levels, size):
|
||||
"""Generate a MultiIndex with natural nesting."""
|
||||
# lowest level of index
|
||||
detailed_index = generate_index_data(dtypes[-1], num_rows)
|
||||
# now make the higher levels, here we want far fewer unique values to generate repeats
|
||||
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 = generate_index_data(dtypes[i], 2 if i==0 else 3)
|
||||
higher_levels.append(np.random.choice(sample, size=num_rows))
|
||||
index_names = np.random.choice(words, levels, replace=False)
|
||||
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)
|
||||
|
||||
# Generate hierarchical MultiIndex with natural grouping
|
||||
if num_index_levels > 1:
|
||||
index = generate_multi_index(index_types, num_index_levels, num_rows)
|
||||
else:
|
||||
name = np.random.choice(words, 1)[0]
|
||||
index = pd.Index(generate_index_data(index_types[0], num_rows), name=name)
|
||||
|
||||
# Data types
|
||||
data_types = ["int", "float", "str", "date", 'datetime']
|
||||
p = np.array([1, 2, 0.5, 0.5, 0.5], dtype=float)
|
||||
p /= p.sum()
|
||||
dtype_choices = np.random.choice(data_types, num_columns, p=p, replace=True)
|
||||
|
||||
# Generate column structure
|
||||
if num_column_levels > 1:
|
||||
columns = generate_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")
|
||||
|
||||
def generate_column_data(dtype):
|
||||
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=num_rows)
|
||||
return np.random.randint(-10000, 10000, size=size)
|
||||
else:
|
||||
return np.random.randint(0, 10**9, size=num_rows)
|
||||
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=num_rows)
|
||||
return 10. ** np.random.uniform(-9, 1, size=size)
|
||||
elif picker < 0.8:
|
||||
return 10. ** np.random.uniform(-1, 10, size=num_rows)
|
||||
return 10. ** np.random.uniform(-1, 10, size=size)
|
||||
else:
|
||||
signs = np.random.choice([-1, 1], size=num_rows)
|
||||
return np.pi ** np.random.uniform(-75, 75, size=num_rows) * signs
|
||||
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(words, size=num_rows)
|
||||
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(num_rows)]
|
||||
return np.random.choice([d.strftime("%Y-%m-%d") for d in dates], size=num_rows)
|
||||
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),
|
||||
@@ -441,219 +220,124 @@ def generate_test_dataframe(
|
||||
minutes=random.randint(0, 59),
|
||||
seconds=random.randint(0, 59),
|
||||
microseconds=random.randint(0, 999999))
|
||||
for _ in range(num_rows)]
|
||||
return np.random.choice([d.strftime("%Y-%m-%d %H:%M:%S.%f") for d in dates], size=num_rows)
|
||||
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))
|
||||
|
||||
# Generate data
|
||||
data = {col: generate_column_data(dtype) for col, dtype in zip(columns, dtype_choices)}
|
||||
df = pd.DataFrame(data, index=index, columns=columns)
|
||||
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.
|
||||
|
||||
# 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")
|
||||
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.
|
||||
|
||||
# Introduce NaNs
|
||||
num_nans = int(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
|
||||
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)))
|
||||
|
||||
# Introduce radical None values
|
||||
num_missing = int(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
|
||||
# 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)
|
||||
|
||||
######################################
|
||||
### roll my own
|
||||
def make_style(self, spacing='medium', debug=False):
|
||||
if debug:
|
||||
head_tb = '#0ff'
|
||||
body_b = '#f0f'
|
||||
h0 = '#f00'
|
||||
h1 = '#b00'
|
||||
h2 = '#900'
|
||||
bh0 = '#f00'
|
||||
bh1 = '#b00'
|
||||
v0 = '#0f0'
|
||||
v1 = '#0a0'
|
||||
v2 = '#090'
|
||||
padt, padr, padb, padl = 2, 10, 2, 10
|
||||
else:
|
||||
head_tb = '#000'
|
||||
body_b = '#000'
|
||||
h0 = '#000'
|
||||
h1 = '#000'
|
||||
h2 = '#000'
|
||||
bh0 = '#000'
|
||||
bh1 = '#000'
|
||||
v0 = '#000'
|
||||
v1 = '#000'
|
||||
v2 = '#000'
|
||||
table_hrule = 2.5
|
||||
if spacing == 'tight':
|
||||
padt, padr, padb, padl = 0, 5, 0, 5
|
||||
elif spacing == 'medium':
|
||||
padt, padr, padb, padl = 2, 10, 2, 10
|
||||
elif spacing == 'loose':
|
||||
padt, padr, padb, padl = 4, 15, 4, 15
|
||||
else:
|
||||
raise ValueError('spacing must be tight, medium or loose')
|
||||
# Data types
|
||||
dtype_choices = np.random.choice(self.data_types, num_columns, p=self.data_type_probs, replace=True)
|
||||
|
||||
style = f'''
|
||||
<style>
|
||||
#{self.df_id} {{
|
||||
border-collapse: collapse;
|
||||
font-family: "Roboto", "Open Sans Condensed", "Arial", 'Segoe UI', sans-serif;
|
||||
font-size: {self.font_size}em;
|
||||
width: auto;
|
||||
border: none;
|
||||
overflow: auto; }}
|
||||
/* tag formats */
|
||||
#{self.df_id} thead {{
|
||||
/* top and bottom of header */
|
||||
border-top: {table_hrule}px solid { head_tb};
|
||||
border-bottom: {table_hrule}px solid {head_tb};
|
||||
}}
|
||||
#{self.df_id} tbody {{
|
||||
/* bottom of body */
|
||||
border-bottom: {table_hrule}px solid {body_b};
|
||||
}}
|
||||
#{self.df_id} tbody th {{
|
||||
vertical-align: top;
|
||||
}}
|
||||
#{self.df_id} caption {{
|
||||
padding-top: 10px;
|
||||
padding-bottom: 4px;
|
||||
font-size: 1.1em;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
caption-side: top;
|
||||
#{self.df_id} td, th {{
|
||||
/* top, right, bottom left cell padding */
|
||||
padding: {padt}px {padr}px {padb}2px {padl}10px;
|
||||
vertical-align: top;
|
||||
}}
|
||||
}}
|
||||
/* class overrides */
|
||||
#{self.df_id} .grt-hrule-0 {{
|
||||
border-top: {self.hrule_widths[0]}px solid {h0};
|
||||
}}
|
||||
#{self.df_id} .grt-hrule-1 {{
|
||||
border-top: {self.hrule_widths[1]}px solid {h1};
|
||||
}}
|
||||
#{self.df_id} .grt-hrule-2 {{
|
||||
border-top: {self.hrule_widths[2]}px solid {h2};
|
||||
}}
|
||||
#{self.df_id} .grt-bhrule-0 {{
|
||||
border-bottom: {self.hrule_widths[0]}px solid {bh0};
|
||||
}}
|
||||
#{self.df_id} .grt-bhrule-1 {{
|
||||
border-bottom: {self.hrule_widths[1]}px solid {bh1};
|
||||
}}
|
||||
#{self.df_id} .grt-vrule-0 {{
|
||||
border-left: {self.vrule_widths[0]}px solid {v0};
|
||||
}}
|
||||
#{self.df_id} .grt-vrule-1 {{
|
||||
border-left: {self.vrule_widths[1]}px solid {v1};
|
||||
}}
|
||||
#{self.df_id} .grt-vrule-2 {{
|
||||
border-left: {self.vrule_widths[2]}px solid {v2};
|
||||
}}
|
||||
#{self.df_id} .grt-left {{
|
||||
text-align: left;
|
||||
}}
|
||||
#{self.df_id} .grt-center {{
|
||||
text-align: center;
|
||||
}}
|
||||
#{self.df_id} .grt-right {{
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}}
|
||||
#{self.df_id} .grt-head {{
|
||||
font-family: "Times New Roman", 'Courier New';
|
||||
font-size: {self.font_size}em;
|
||||
}}
|
||||
</style>
|
||||
'''
|
||||
return style
|
||||
# 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)
|
||||
|
||||
def df_to_html(self, spacing='medium', debug=False):
|
||||
"""Convert a pandas DataFrame to an HTML table with sparsification."""
|
||||
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
|
||||
# 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")
|
||||
|
||||
# Start table
|
||||
html = [f'<table id="{self.df_id}">']
|
||||
# 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
|
||||
|
||||
# Process header
|
||||
bit = self.df.T.reset_index(drop=False)
|
||||
idx_header = bit.iloc[:self.nindex, :self.ncolumns]
|
||||
columns = bit.iloc[self.nindex:, :self.ncolumns]
|
||||
# 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
|
||||
|
||||
# this is TRANSPOSED!!
|
||||
html.append("<thead>")
|
||||
for i in range(self.ncolumns):
|
||||
# one per row of columns m index, usually only 1
|
||||
# TODO Add header aligners
|
||||
html.append("<tr>")
|
||||
for j, r in enumerate(idx_header.iloc[:, i]):
|
||||
# columns one per level of index
|
||||
html.append(f'<th class="grt-left">{r}</th>')
|
||||
for j, r in enumerate(columns.iloc[:, i]):
|
||||
# one per column of dataframe
|
||||
# figure how high up mindex the vrules go
|
||||
# all headings get hrules, it's the vrules that are tricky
|
||||
hrule = f'grt-bhrule-{i}' if i < self.ncolumns - 1 else ''
|
||||
if 0 < j < self.ncols and i >= column_change_level[j]:
|
||||
vrule = f'grt-vrule-{column_change_level[j]}'
|
||||
elif j == 0:
|
||||
# start with the first column come what may
|
||||
vrule = f'grt-vrule-{column_change_level[0]}'
|
||||
else:
|
||||
vrule = ''
|
||||
html.append(f'<th class="grt-center {hrule} {vrule}">{r}</th>')
|
||||
html.append("</tr>")
|
||||
html.append("</thead>")
|
||||
df = df.sort_index().sort_index(axis=1)
|
||||
return df
|
||||
|
||||
html.append("<tbody>")
|
||||
for i, (n, r) in enumerate(self.df.iterrows()):
|
||||
# one per row of dataframe
|
||||
html.append("<tr>")
|
||||
hrule = ''
|
||||
for j, c in enumerate(r.iloc[:self.nindex]):
|
||||
# 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]:
|
||||
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>')
|
||||
for j, c in enumerate(r.iloc[self.nindex:]):
|
||||
# first col left handled by index/body divider
|
||||
if 0 < j < self.ncols:
|
||||
vrule = f'grt-vrule-{column_change_level[j]}'
|
||||
elif j == 0:
|
||||
# start with the first column come what may
|
||||
vrule = f'grt-vrule-{column_change_level[0]}'
|
||||
html.append(f'<td class="grt-data-r-{i} grt-data-c-{j} {self.df_aligners[j+self.nindex]} {hrule} {vrule}">{c}</td>')
|
||||
html.append("</tr>")
|
||||
html.append("</tbody>")
|
||||
return '\n'.join(html)
|
||||
__call__ = make_test_dataframe
|
||||
|
||||
def test_suite(self):
|
||||
"""Make a dict of test dataframes with different characteristics."""
|
||||
ans = {}
|
||||
|
||||
def to_html(self, spacing='medium', debug=False):
|
||||
"""Full monty, raw string."""
|
||||
html = df_to_html(self, spacing=spacing, debug=debug)
|
||||
style = make_style(self, spacing=spacing, debug=debug)
|
||||
return style + html
|
||||
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'])
|
||||
|
||||
def process(self, spacing='medium', debug=False):
|
||||
"""Full monty."""
|
||||
return HTML(to_html(self, spacing=spacing, debug=debug))
|
||||
ans['multiindex'] = self.make_test_dataframe(num_rows=10, num_columns=5,
|
||||
num_index_levels=3, num_column_levels=1,
|
||||
column_name_length=4, title=True, sep=' ',
|
||||
index_types=['int', 'str'])
|
||||
|
||||
ans['multicolumns'] = self.make_test_dataframe(num_rows=10, num_columns=5,
|
||||
num_index_levels=1, num_column_levels=3,
|
||||
column_name_length=4, title=True, sep=' ',
|
||||
index_types=['int', 'str'])
|
||||
|
||||
ans['complex'] = self.make_test_dataframe(num_rows=20, num_columns=10,
|
||||
num_index_levels=3, num_column_levels=3,
|
||||
column_name_length=4,
|
||||
index_types=['int', 'str'])
|
||||
|
||||
return ans
|
||||
|
||||
Reference in New Issue
Block a user