Skip to content

Commit 46c00ea

Browse files
Make sorting_variable optional for factor_library download
Calling download_data("Tidy Finance", "factor_library") without a sorting_variable now returns the default portfolio construction for all sorting variables instead of raising. Other defaults still apply; pass fill_all=True to leave every column unrestricted. Closes #55 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 71811b9 commit 46c00ea

4 files changed

Lines changed: 81 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased
44

5+
- **`sorting_variable` is now optional for `factor_library`:** Calling
6+
`download_data("Tidy Finance", "factor_library")` without a
7+
`sorting_variable` now returns the default portfolio construction for
8+
all sorting variables instead of raising an error. Other defaults
9+
(e.g. `rebalancing`, `weighting_scheme`) still apply; pass
10+
`fill_all=True` to leave every column unrestricted.
511
- **Dependencies (replaced pyfixest with formulaic):** The `pyfixest` dependency was dropped in favor of [`formulaic`](https://github.com/matthewwardrop/formulaic) plus a small internal numpy OLS helper (`_fit_ols`). `pyfixest` was used only for plain OLS with classical (IID) standard errors in `estimate_model` and the cross-sectional / IID-variance steps of `estimate_fama_macbeth`, but it pulled in `great-tables``multimark`, a `cffi` C-extension that ships no Python 3.14 wheels and therefore required a C/C++ toolchain (e.g. MSVC Build Tools on Windows) to install on unsupported interpreters. `_fit_ols` builds the design matrix via `formulaic` and reproduces `feols` coefficients, standard errors, t-statistics, and residuals to ~1e-9 for models without fixed effects, so results are unchanged. Note that `estimate_model` and `estimate_fama_macbeth` now perform classical OLS only (the fixed-effects / clustered-SE features of `pyfixest` were never used and are no longer available).
612

713
## v0.1.0

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = [{name = "Christoph Frey", email = "christoph.frey@gmail.com"},
88
{name = "Christoph Scheuch", email = "christoph@tidy-intelligence.com"},
99
{name = "Stefan Voigt", email = "stefan.voigt@econ.ku.dk"}
1010
]
11-
version = "0.3.1.dev0"
11+
version = "0.3.1.dev1"
1212
description = "Tidy Finance Helper Functions"
1313
readme = "README.md"
1414
license = "MIT"

tests/test_download_data_huggingface.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
1212
)
1313

14-
from tidyfinance.download_tidy_finance import _download_data_huggingface, _download_data_huggingface_factor_library, _download_factor_library_grid, _download_factor_library_ids, _filter_factor_library_grid, _get_available_huggingface_files # noqa: E402
14+
from tidyfinance.download_tidy_finance import (
15+
_download_data_huggingface,
16+
_download_data_huggingface_factor_library,
17+
_download_factor_library_grid,
18+
_download_factor_library_ids,
19+
_filter_factor_library_grid,
20+
_get_available_huggingface_files,
21+
) # noqa: E402
1522

1623

1724
def _make_grid(grid_id=1):
@@ -51,7 +58,8 @@ def test_single_page_returns_only_parquet_files():
5158
response_mock.raise_for_status = MagicMock()
5259

5360
with patch(
54-
"tidyfinance.download_tidy_finance.requests.get", return_value=response_mock
61+
"tidyfinance.download_tidy_finance.requests.get",
62+
return_value=response_mock,
5563
):
5664
result = _get_available_huggingface_files("org", "ds")
5765

@@ -78,7 +86,9 @@ def test_multi_page_paginates_until_rel_next_link_absent():
7886
r2.raise_for_status = MagicMock()
7987
responses.append(r2)
8088

81-
with patch("tidyfinance.download_tidy_finance.requests.get", side_effect=responses):
89+
with patch(
90+
"tidyfinance.download_tidy_finance.requests.get", side_effect=responses
91+
):
8292
result = _get_available_huggingface_files("org", "ds")
8393

8494
assert len(result) == 2
@@ -237,6 +247,43 @@ def test_aborts_non_univariate_sort_without_secondary_n():
237247
)
238248

239249

250+
def test_sorting_variable_optional_returns_all_with_defaults():
251+
"""Test sorting_variable omitted: all sorting variables, defaults."""
252+
grid = pd.DataFrame(
253+
{
254+
"id": [1, 2, 3],
255+
"sorting_variable": ["sv_me", "sv_bm", "sv_me"],
256+
"min_size_quantile": [0.2, 0.2, 0.4],
257+
"exclude_financials": [False, False, False],
258+
"exclude_utilities": [False, False, False],
259+
"exclude_negative_earnings": [False, False, False],
260+
"sorting_variable_lag": ["6m", "6m", "6m"],
261+
"rebalancing": ["monthly", "monthly", "monthly"],
262+
"n_portfolios_main": [10, 10, 10],
263+
"sorting_method": ["univariate", "univariate", "univariate"],
264+
"n_portfolios_secondary": [None, None, None],
265+
"breakpoints_exchanges": ["NYSE", "NYSE", "NYSE"],
266+
"breakpoints_min_size_threshold": [None, None, None],
267+
"weighting_scheme": ["VW", "VW", "VW"],
268+
}
269+
)
270+
available = pd.DataFrame({"path": ["grid.parquet"], "size": [100]})
271+
with (
272+
patch(
273+
"tidyfinance.download_tidy_finance._get_available_huggingface_files",
274+
return_value=available,
275+
),
276+
patch(
277+
"tidyfinance.download_tidy_finance.pd.read_parquet",
278+
return_value=grid,
279+
),
280+
):
281+
ids = _filter_factor_library_grid()
282+
283+
# Both sorting variables returned; the non-default row (id 3) dropped.
284+
assert ids == [1, 2]
285+
286+
240287
def test_fill_all_false_defaults_applied_row_filtered_out():
241288
"""Test fill_all = FALSE: defaults applied, row filtered out."""
242289
grid = pd.DataFrame(
@@ -263,7 +310,10 @@ def test_fill_all_false_defaults_applied_row_filtered_out():
263310
"tidyfinance.download_tidy_finance._get_available_huggingface_files",
264311
return_value=available,
265312
),
266-
patch("tidyfinance.download_tidy_finance.pd.read_parquet", return_value=grid),
313+
patch(
314+
"tidyfinance.download_tidy_finance.pd.read_parquet",
315+
return_value=grid,
316+
),
267317
):
268318
ids = _filter_factor_library_grid(sorting_variable="me")
269319

@@ -296,7 +346,10 @@ def test_fill_all_true_only_explicit_filters_applied():
296346
"tidyfinance.download_tidy_finance._get_available_huggingface_files",
297347
return_value=available,
298348
),
299-
patch("tidyfinance.download_tidy_finance.pd.read_parquet", return_value=grid),
349+
patch(
350+
"tidyfinance.download_tidy_finance.pd.read_parquet",
351+
return_value=grid,
352+
),
300353
):
301354
ids = _filter_factor_library_grid(sorting_variable="me", fill_all=True)
302355

@@ -316,7 +369,8 @@ def test_pulls_url_from_available_files_and_reads_parquet():
316369
return_value=available,
317370
),
318371
patch(
319-
"tidyfinance.download_tidy_finance.pd.read_parquet", return_value=mock_grid
372+
"tidyfinance.download_tidy_finance.pd.read_parquet",
373+
return_value=mock_grid,
320374
),
321375
):
322376
result = _download_factor_library_grid()
@@ -335,7 +389,10 @@ def test_aborts_when_no_grid_rows_match_requested_ids():
335389
"tidyfinance.download_tidy_finance._get_available_huggingface_files",
336390
return_value=available,
337391
),
338-
patch("tidyfinance.download_tidy_finance.pd.read_parquet", return_value=grid),
392+
patch(
393+
"tidyfinance.download_tidy_finance.pd.read_parquet",
394+
return_value=grid,
395+
),
339396
):
340397
with pytest.raises(ValueError):
341398
_download_factor_library_ids([999])
@@ -352,7 +409,10 @@ def test_aborts_when_ids_have_no_matching_parquet_file():
352409
"tidyfinance.download_tidy_finance._get_available_huggingface_files",
353410
return_value=available,
354411
),
355-
patch("tidyfinance.download_tidy_finance.pd.read_parquet", return_value=grid),
412+
patch(
413+
"tidyfinance.download_tidy_finance.pd.read_parquet",
414+
return_value=grid,
415+
),
356416
):
357417
with pytest.raises(ValueError):
358418
_download_factor_library_ids([1])

tidyfinance/download_tidy_finance.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,8 @@ def _filter_factor_library_grid(fill_all: bool = False, **filters) -> list:
271271
grid. Each value may be a scalar or a list/tuple to match multiple
272272
levels. Supported columns and their defaults are:
273273
274-
- 'sorting_variable': no default, required.
274+
- 'sorting_variable': no default. When omitted, all sorting
275+
variables are returned (subject to the remaining defaults).
275276
- 'min_size_quantile': 0.2
276277
- 'exclude_financials': False
277278
- 'exclude_utilities': False
@@ -303,9 +304,6 @@ def _filter_factor_library_grid(fill_all: bool = False, **filters) -> list:
303304
f"Supported filters: {list(_FACTOR_LIBRARY_SUPPORTED_FILTERS)}."
304305
)
305306

306-
if "sorting_variable" not in filters:
307-
raise ValueError("'sorting_variable' is required in filters.")
308-
309307
if filters.get("sorting_method", "univariate") != "univariate":
310308
if filters.get("n_portfolios_secondary") is None:
311309
raise ValueError(
@@ -577,9 +575,11 @@ def _download_data_huggingface(
577575
matches the intended design. Supported filter columns and their
578576
defaults are:
579577
580-
- 'sorting_variable': required. The firm characteristic used to
578+
- 'sorting_variable': optional. The firm characteristic used to
581579
sort stocks into portfolios (e.g., 'me' for market equity, 'bm'
582-
for book-to-market). No default is applied.
580+
for book-to-market). No default is applied; when omitted, all
581+
sorting variables are returned (subject to the remaining
582+
defaults).
583583
- 'min_size_quantile' (defaults to 0.2): fraction of the smallest
584584
stocks (by market cap) excluded from the portfolio universe; 0.2
585585
drops the bottom 20%.

0 commit comments

Comments
 (0)