Spaces:
Running
Running
| from collections.abc import Iterable | |
| import pandas as pd | |
| import streamlit as st | |
| # Visual marker prepended to a company label when its latest filed year lags the | |
| # dataset's max year — signals "this company's most recent numbers are stale". | |
| STALE_MARKER = "⚠" # ⚠ | |
| def _as_int_year(value) -> int | None: | |
| """Coerce a possibly-None / NaN / str year to int, or None if not parseable.""" | |
| if value is None: | |
| return None | |
| if isinstance(value, float) and pd.isna(value): | |
| return None | |
| try: | |
| return int(value) | |
| except (TypeError, ValueError): | |
| return None | |
| def is_stale(latest_year, dataset_max_year) -> bool: | |
| """True when a company's latest filed year is older than the dataset max year. | |
| A company is "stale" if its most recent filing predates the newest year | |
| present anywhere in the dataset — i.e. it stopped reporting before the | |
| frontier. Unknown / unparseable latest years are treated as NOT stale | |
| (we don't flag what we can't measure). | |
| """ | |
| ly = _as_int_year(latest_year) | |
| my = _as_int_year(dataset_max_year) | |
| if ly is None or my is None: | |
| return False | |
| return ly < my | |
| def stale_count(latest_years: Iterable, dataset_max_year) -> tuple[int, int]: | |
| """Return ``(n_stale, n_total)`` over an iterable of latest-filed years. | |
| ``n_total`` counts only companies with a parseable latest year; companies | |
| whose latest year is unknown are excluded from BOTH numerator and | |
| denominator so the caption reads honestly. | |
| """ | |
| my = _as_int_year(dataset_max_year) | |
| n_total = 0 | |
| n_stale = 0 | |
| for ly in latest_years: | |
| ly_i = _as_int_year(ly) | |
| if ly_i is None or my is None: | |
| continue | |
| n_total += 1 | |
| if ly_i < my: | |
| n_stale += 1 | |
| return n_stale, n_total | |
| def stale_caption(latest_years: Iterable, dataset_max_year) -> str | None: | |
| """Build a caption like '12 of 50 companies last filed before FY2024'. | |
| Returns ``None`` when nothing is stale (or the dataset max year is unknown), | |
| so callers can omit the caption entirely in the clean case. | |
| """ | |
| my = _as_int_year(dataset_max_year) | |
| if my is None: | |
| return None | |
| n_stale, n_total = stale_count(latest_years, dataset_max_year) | |
| if n_stale <= 0 or n_total <= 0: | |
| return None | |
| return ( | |
| f"{STALE_MARKER} {n_stale} of {n_total} companies last filed before " | |
| f"FY{my} (marked {STALE_MARKER})." | |
| ) | |
| def display_decimals() -> int: | |
| """Read the user-chosen decimal precision (0/1/2) from session state.""" | |
| return int(st.session_state.get("display_decimals", 0)) | |
| def fmt_k_gel(v) -> str: | |
| """Format a raw GEL value as thousands. Blank for 0/None/NaN; parens for negatives. | |
| Decimal count is read from session state (the sidebar "Decimal precision" | |
| selector) so the user can flip 0/1/2 globally without re-rendering wiring. | |
| """ | |
| if v is None: | |
| return "" | |
| if isinstance(v, float) and pd.isna(v): | |
| return "" | |
| try: | |
| f = float(v) / 1000.0 | |
| except (TypeError, ValueError): | |
| return str(v) | |
| d = display_decimals() | |
| if d == 0 and abs(f) < 0.5: | |
| # Avoid rendering tiny values that round to zero at 0-decimal precision | |
| # as the bare string "0" — keep them blank like true zeros for clarity. | |
| return "" | |
| if f == 0: | |
| return "" | |
| if f < 0: | |
| return f"({abs(f):,.{d}f})" | |
| return f"{f:,.{d}f}" | |
| def fmt_pct(v) -> str: | |
| """Format a decimal proportion (0.123) as a percentage (12.3% at 1 decimal). | |
| Decimal count follows the same session-state setting as `_fmt`. | |
| """ | |
| if v is None: | |
| return "" | |
| if isinstance(v, float) and pd.isna(v): | |
| return "" | |
| if v == 0: | |
| return "" | |
| d = display_decimals() | |
| return f"{v * 100:,.{d}f}%" | |
| def fmt_pct_signed(v, min_decimals: int = 1) -> str: | |
| """Margin-style percent: parenthesized negatives, blank for 0/None/NaN. | |
| THE shared formatter for statement margin rows / common-size cells / CAGR | |
| percents — previously four near-identical private copies had drifted | |
| (2026-07-02 review). Decimals = max(global "Decimal precision" setting, | |
| ``min_decimals``): the user setting can RAISE precision, but statement | |
| tables never drop below 1 decimal (0-decimal margins are unreadable). | |
| """ | |
| if v is None: | |
| return "" | |
| if isinstance(v, float) and pd.isna(v): | |
| return "" | |
| if v == 0: | |
| return "" | |
| d = max(display_decimals(), min_decimals) | |
| if v < 0: | |
| return f"({abs(v) * 100:,.{d}f}%)" | |
| return f"{v * 100:,.{d}f}%" | |
| def fmt_money_compact(v) -> str: | |
| """Compact ₾ for KPI tiles: ``₾1.2bn`` / ``₾45.1m`` / ``₾6,200k``. | |
| Returns an em-dash for None/NaN (KPI tiles need a visible placeholder, | |
| unlike table cells which blank). Decimals follow the global "Decimal | |
| precision" setting, floored at 1 for bn/m so the compact form never | |
| collapses to a bare ``₾1bn``. | |
| """ | |
| if v is None or (isinstance(v, float) and pd.isna(v)): | |
| return "—" | |
| try: | |
| f = float(v) | |
| except (TypeError, ValueError): | |
| return str(v) | |
| d = display_decimals() | |
| a = abs(f) | |
| if a >= 1e9: | |
| return f"₾{f / 1e9:,.{max(d, 1)}f}bn" | |
| if a >= 1e6: | |
| return f"₾{f / 1e6:,.{max(d, 1)}f}m" | |
| if a >= 1e3: | |
| return f"₾{f / 1e3:,.{d}f}k" | |
| return f"₾{f:,.{d}f}" | |