Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@ compare_themes.py
compare_tables.html

README_FILES/
uv.lock
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
.PHONY: check

test:
pytest --cov-report=xml
@uv run pytest --cov-report=xml

test-update:
pytest --snapshot-update
@uv run pytest --snapshot-update

lint: ## Run ruff linter and type checker
@uv run ruff check --fix
@uv run ruff format
@uv run ty check .

pre-commit-install: ## Install pre-commit hooks
@uv run pre-commit install

pre-commit: ## Run pre-commit hooks
@uv run pre-commit run --all-files

docs-build:
cd docs \
Expand Down
1 change: 1 addition & 0 deletions docs/examples/nfl-season/index.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"\n",
" return results\n",
"\n",
"\n",
"team_stats = get_team_stats(season_2016)"
]
},
Expand Down
205 changes: 205 additions & 0 deletions gt_extras/_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"""Native Python implementations of statistical functions.

This module provides pure Python implementations of statistical functions
previously provided by scipy, specifically for computing confidence intervals.
"""

from __future__ import annotations

import math


def mean(values: list[float | int]) -> float:
"""Compute the arithmetic mean of a list of values."""
if not values:
raise ValueError("Cannot compute mean of empty list")
return sum(values) / len(values)


def std(values: list[float | int], ddof: int = 1) -> float:
"""Compute the sample standard deviation.

Parameters
----------
values
List of numeric values.
ddof
Delta degrees of freedom. Default is 1 for sample std.

Returns
-------
float
The sample standard deviation.
"""
n = len(values)
if n <= ddof:
raise ValueError(
f"Need at least {ddof + 1} values to compute std with ddof={ddof}"
)
m = mean(values)
variance = sum((x - m) ** 2 for x in values) / (n - ddof)
return math.sqrt(variance)


def sem(values: list[float | int]) -> float:
"""Compute the standard error of the mean.

Parameters
----------
values
List of numeric values.

Returns
-------
float
The standard error of the mean (std / sqrt(n)).
"""
n = len(values)
if n < 2:
raise ValueError("Need at least 2 values to compute sem")
return std(values, ddof=1) / math.sqrt(n)


def _norm_ppf(p: float) -> float:
"""Approximate the inverse of the standard normal CDF (probit function).

Uses the Abramowitz and Stegun approximation (formula 26.2.23).
Accurate to about 4.5 decimal places.

Parameters
----------
p
Probability value between 0 and 1.

Returns
-------
float
The z-score corresponding to the probability p.
"""
if p <= 0 or p >= 1:
if p == 0:
return float("-inf")
if p == 1:
return float("inf")
raise ValueError("p must be between 0 and 1")

# Constants for approximation
a0 = 2.515517
a1 = 0.802853
a2 = 0.010328
b1 = 1.432788
b2 = 0.189269
b3 = 0.001308

if p < 0.5:
sign = -1
p_work = p
else:
sign = 1
p_work = 1 - p

if p_work < 1e-300:
return sign * 38.0 # Approximate for extreme values

t = math.sqrt(-2 * math.log(p_work))
numerator = a0 + a1 * t + a2 * t * t
denominator = 1 + b1 * t + b2 * t * t + b3 * t * t * t
result = t - numerator / denominator

return sign * result


def _t_ppf(p: float, df: float) -> float:
"""Approximate the inverse of the Student's t-distribution CDF.

Uses Hill's approximation for the t-distribution quantile function,
which provides good accuracy for most practical purposes.

Parameters
----------
p
Probability value between 0 and 1.
df
Degrees of freedom.

Returns
-------
float
The t-value corresponding to probability p.
"""
if df <= 0:
raise ValueError("Degrees of freedom must be positive")

# For very large df, use normal approximation
if df > 1e6:
return _norm_ppf(p)

# Get the corresponding normal quantile
z = _norm_ppf(p)

# For df=1 (Cauchy), use tan approximation
if df == 1:
return math.tan(math.pi * (p - 0.5))

# For df=2, there's an exact formula
if df == 2:
alpha = 2 * p - 1
if abs(alpha) >= 1:
return float("inf") if alpha > 0 else float("-inf")
return alpha / math.sqrt(2 * (1 - alpha * alpha))

# Hill's approximation for general df
# Cornish-Fisher expansion
g1 = (z**3 + z) / 4
g2 = (5 * z**5 + 16 * z**3 + 3 * z) / 96
g3 = (3 * z**7 + 19 * z**5 + 17 * z**3 - 15 * z) / 384
g4 = (79 * z**9 + 776 * z**7 + 1482 * z**5 - 1920 * z**3 - 945 * z) / 92160

inv_df = 1 / df
t = z + g1 * inv_df + g2 * inv_df**2 + g3 * inv_df**3 + g4 * inv_df**4

return t


def t_interval(
confidence: float,
df: int,
loc: float = 0.0,
scale: float = 1.0,
) -> tuple[float, float]:
"""Compute the confidence interval for a t-distribution.

This is equivalent to scipy.stats.t.interval().

Parameters
----------
confidence
The confidence level (e.g., 0.95 for 95% CI).
df
Degrees of freedom.
loc
Location parameter (mean). Default is 0.
scale
Scale parameter (standard error). Default is 1.

Returns
-------
tuple[float, float]
A tuple (lower, upper) representing the confidence interval bounds.
"""
if not 0 < confidence < 1:
raise ValueError("Confidence must be between 0 and 1")
if df < 1:
raise ValueError("Degrees of freedom must be at least 1")

alpha = 1 - confidence
lower_tail = alpha / 2
upper_tail = 1 - alpha / 2

t_lower = _t_ppf(lower_tail, df)
t_upper = _t_ppf(upper_tail, df)

lower = loc + scale * t_lower
upper = loc + scale * t_upper

return (lower, upper)
5 changes: 3 additions & 2 deletions gt_extras/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
)
from great_tables._locations import resolve_cols_c
from great_tables._tbl_data import SelectExpr, is_na
from scipy.stats import sem, t, tmean
from svg import (
SVG,
Arc,
Expand All @@ -27,6 +26,8 @@
)

from gt_extras import gt_duplicate_column
from gt_extras._stats import mean as tmean
from gt_extras._stats import sem, t_interval
from gt_extras._utils_color import _get_discrete_colors_from_palette
from gt_extras._utils_column import (
_format_numeric_text,
Expand Down Expand Up @@ -884,7 +885,7 @@ def _compute_mean_and_conf_int(val):
if val is None or not isinstance(val, list) or len(val) == 0:
return (None, None, None)
mean = tmean(val)
conf_int = t.interval(
conf_int = t_interval(
ci,
len(val) - 1,
loc=mean,
Expand Down
9 changes: 9 additions & 0 deletions gt_extras/tests/__snapshots__/test_colors.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,24 @@
# name: test_gt_data_color_by_group_multiple_groups_snap[pd_and_pl]
'''
<tbody class="gt_table_body">
<tr class="gt_group_heading_row">
<th class="gt_group_heading" colspan="1">1</th>
</tr>
<tr>
<td style="color: #FFFFFF; background-color: #000000;" class="gt_row gt_right">1</td>
</tr>
<tr class="gt_group_heading_row">
<th class="gt_group_heading" colspan="1">2</th>
</tr>
<tr>
<td style="color: #FFFFFF; background-color: #000000;" class="gt_row gt_right">2</td>
</tr>
<tr>
<td style="color: #000000; background-color: #9e9e9e;" class="gt_row gt_right">3</td>
</tr>
<tr class="gt_group_heading_row">
<th class="gt_group_heading" colspan="1">3</th>
</tr>
<tr>
<td style="color: #FFFFFF; background-color: #000000;" class="gt_row gt_right">4</td>
</tr>
Expand Down
15 changes: 12 additions & 3 deletions gt_extras/tests/__snapshots__/test_formatting.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@
#mycombinedtable .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; }
#mycombinedtable .gt_row_group_first td { border-top-width: 2px; }
#mycombinedtable .gt_row_group_first th { border-top-width: 2px; }
#mycombinedtable .gt_striped { background-color: rgba(128,128,128,0.05); }
#mycombinedtable .gt_striped { color: #333333; background-color: #F4F4F4; }
#mycombinedtable .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#mycombinedtable .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#mycombinedtable .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#mycombinedtable .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#mycombinedtable .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
#mycombinedtable .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#mycombinedtable .gt_left { text-align: left; }
Expand Down Expand Up @@ -182,8 +185,11 @@
#id1 .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; }
#id1 .gt_row_group_first td { border-top-width: 2px; }
#id1 .gt_row_group_first th { border-top-width: 2px; }
#id1 .gt_striped { background-color: rgba(128,128,128,0.05); }
#id1 .gt_striped { color: #333333; background-color: #F4F4F4; }
#id1 .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#id1 .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#id1 .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#id1 .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#id1 .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
#id1 .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#id1 .gt_left { text-align: left; }
Expand Down Expand Up @@ -256,8 +262,11 @@
#id2 .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; }
#id2 .gt_row_group_first td { border-top-width: 2px; }
#id2 .gt_row_group_first th { border-top-width: 2px; }
#id2 .gt_striped { background-color: rgba(128,128,128,0.05); }
#id2 .gt_striped { color: #333333; background-color: #F4F4F4; }
#id2 .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#id2 .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#id2 .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#id2 .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#id2 .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
#id2 .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#id2 .gt_left { text-align: left; }
Expand Down
17 changes: 7 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,21 @@ license.file = "LICENSE"
dependencies = [
"faicons>=0.2.2",
"great-tables>=0.18.0",
"scipy>=1.13.1",
"svg-py>=1.6.0",
"narwhals>=1.0.0",
]

authors = [
{ name = "Jules Walzer-Goldfeld", email = "jules.walzergoldfeld@gmail.com"},
{ name = "Jules Walzer-Goldfeld", email = "jules.walzergoldfeld@gmail.com" },
{ name = "Michael Chow", email = "mc_al_github@fastmail.com" },
{ name = "Rich Iannone", email = "rich@posit.co"}
{ name = "Rich Iannone", email = "rich@posit.co" },
]

classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12"
"Programming Language :: Python :: 3.12",
]
keywords = ["tables", "html"]

Expand All @@ -52,14 +51,12 @@ dev = [
"quartodoc>=0.11.1",
"quarto>=0.1.0",
"pre-commit>=4.2.0",
"ruff>=0.14.10",
"ty>=0.0.8",
]


[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:"
]
exclude_also = ["if TYPE_CHECKING:"]
include = ["gt_extras/*"]
omit = [
"gt_extras/tests/*"
]
omit = ["gt_extras/tests/*"]
Loading