diff --git a/examples/team_recommender/tests/helpers.py b/examples/team_recommender/tests/helpers.py index 8c07c8a..a4c37ec 100644 --- a/examples/team_recommender/tests/helpers.py +++ b/examples/team_recommender/tests/helpers.py @@ -79,6 +79,10 @@ def process_row(row: tuple[int, int, float]) -> str: return f"{row[0]} failures out of {row[1]} is within {row[2] * 100:.0f}% success rate" +def is_statistically_significant(success_rate: float, failure_count: int, sample_size: int) -> bool: + return not is_within_expected(success_rate, failure_count, sample_size) + + def is_within_expected(success_rate: float, failure_count: int, sample_size: int) -> bool: print(f"is_within_expected({success_rate}, {failure_count}, {sample_size})") if sample_size <= 1: diff --git a/examples/team_recommender/tests/test_helpers.py b/examples/team_recommender/tests/test_helpers.py index 900bee3..da15810 100644 --- a/examples/team_recommender/tests/test_helpers.py +++ b/examples/team_recommender/tests/test_helpers.py @@ -5,6 +5,7 @@ _assert_success_rate, failures_within_margin_of_error_from_expected, generate_examples, + is_statistically_significant, is_within_expected, natural_sort_key, ) @@ -142,12 +143,12 @@ def test_seventy_percent_confidence_ranges_from_fifty_to_ninety(): def next_success_rate(sample_size) -> float: - return 1 - 1 / (sample_size + 1) + return sample_size / (sample_size + 1) def test_next_success_rate(): assert next_success_rate(1) == 0.5 - assert next_success_rate(2) == 0.6666666666666667 + assert next_success_rate(2) == pytest.approx(0.6667, rel=0.01) assert next_success_rate(3) == 0.75 assert next_success_rate(4) == 0.8 assert next_success_rate(10) == 0.9090909090909091 @@ -173,36 +174,49 @@ def test_largest_sample_size_for_given_success_rate(success_rate, largest_sample ) -def test_next_sample_size(): - ## Next sample size should be larger than the current one by at least 4 times - assert next_sample_size(10) == 45, ( - "passing 10 out of 10 should require 45 successful runs to be statistically significant" - ) - assert next_sample_size(45) == 185, ( - "passing 45 out of 45 should require 185 successful runs to be statistically significant" +@pytest.mark.parametrize( + "sample_size, expected", + [ + (10, 45), + (45, 185), + (185, 745), + (745, 2985), + (2985, 11945), # 1/11945=0.00008372 = 99.99% + ], +) +def test_next_sample_size_with_1_failure(sample_size, expected): + assert next_sample_size_with_1_failure(sample_size) == expected + + +def test_next_sample_size_via_loop_with_1_failure(): + assert next_sample_size_with_1_failure(29) == next_sample_size_via_loop_with_1_failure(29), ( + "calculated via loop should match" ) - assert next_sample_size(185) == 745 - assert next_sample_size(745) == 2985 - assert next_sample_size(29) == 121 - assert next_sample_size(29) == next_sample_size_via_loop(29), "calculated via loop should match" - assert 28 / 29 == pytest.approx(0.96, rel=0.01) - before = analyse_measure_from_test_sample(28, 29) - assert before.proportion == pytest.approx(0.96, rel=0.01) - assert before.confidence_interval_prop == pytest.approx((0.91, 1.00), 0.01) - analysis = analyse_measure_from_test_sample(120, 121) - assert analysis.proportion == pytest.approx(0.99, rel=0.01) - assert analysis.confidence_interval_prop == pytest.approx((0.98, 1.00), 0.01) +def test_next_success_after_29_runs_is_121(): + starting_runs = 29 + starting_success_rate = (starting_runs - 1) / starting_runs + starting_analysis = analyse_measure_from_test_sample(starting_runs - 1, starting_runs) + assert starting_analysis.proportion == pytest.approx(starting_success_rate) -def next_sample_size(current): + next_size = next_sample_size_with_1_failure(starting_runs) + assert next_size == 121, "should be 121" + next_analysis = analyse_measure_from_test_sample(next_size - 1, next_size) + + assert next_analysis.proportion == pytest.approx(next_success_rate(next_size), rel=0.0001), ( + "analysis proportion should match next rate" + ) + + +def next_sample_size_with_1_failure(sample_size: int) -> int: ## How many successful runs are needed to be statistically significant improvement - # compared to the current sample size with 100% success rate - return 4 * current + 5 + # compared to the current sample size with 100% success rate at 90% confidence + return 4 * sample_size + 5 -def next_sample_size_via_loop(sample_size: int) -> int: +def next_sample_size_via_loop_with_1_failure(sample_size: int) -> int: goal_success_rate = next_success_rate(sample_size) for i in range(sample_size, 5 * sample_size): if not is_within_expected(goal_success_rate, 1, i): @@ -210,6 +224,46 @@ def next_sample_size_via_loop(sample_size: int) -> int: return 0 +def next_sample_size_via_loop_no_failure(sample_size: int) -> int: + goal_success_rate = next_success_rate(sample_size) + for i in range(sample_size, 5 * sample_size): + if not is_within_expected(goal_success_rate, 0, i): + return i + return 0 + + +def next_sample_size_no_failure(sample_size: int) -> int: + return 2 * sample_size + 3 + + +@pytest.mark.parametrize( + "sample_size, expected", + [ + (10, 45), + (45, 185), + (185, 745), + (745, 2985), + (29, 121), + ], +) +def test_next_sample_size_via_loop(sample_size, expected): + assert next_sample_size_via_loop_with_1_failure(sample_size) == expected + + +@pytest.mark.parametrize( + "sample_size, expected", + [ + (10, 23), + (23, 49), + (49, 101), + (101, 205), + (205, 413), + ], +) +def test_next_no_failure_sample_size_via_loop(sample_size, expected): + assert next_sample_size_via_loop_no_failure(sample_size) == expected + + def test_success_rate(): tiny_set_analysis = analyse_measure_from_test_sample(1, 2) assert tiny_set_analysis.proportion == 0.5 @@ -257,3 +311,18 @@ def test_sort_names_with_numbers(): assert sorted([Path(p) for p in unsorted], key=natural_sort_key) == [ Path(p) for p in correctly_sorted ], "example_10_threshold should be last, while example_1_text_response should be first" + + +def test_example_on_wiki(): + sample_size = 47 + success_rate = 0.950 + assert not is_statistically_significant(success_rate, 1, sample_size) + next_rate = next_success_rate(sample_size) + next_size = next_sample_size_no_failure(sample_size) + assert next_sample_size_via_loop_with_1_failure(sample_size) == 193 + assert next_size == 97 + assert next_rate == pytest.approx(0.98, rel=0.01) + + assert not is_within_expected(0.95, 1, next_size) + assert not is_within_expected(next_rate, 0, next_size) + assert is_within_expected(next_rate, 1, next_size) diff --git a/examples/team_recommender/tests/test_proportions_ztest.py b/examples/team_recommender/tests/test_proportions_ztest.py deleted file mode 100644 index 808014b..0000000 --- a/examples/team_recommender/tests/test_proportions_ztest.py +++ /dev/null @@ -1,108 +0,0 @@ -import pytest -from helpers import is_within_expected -from statsmodels.stats.proportion import proportions_ztest -from test_helpers import next_success_rate - - -def test_proportions_ztest_improvement(): - successes = [70, 90] - n_observations = [100, 100] - - stat, p_value = proportions_ztest(successes, n_observations) - assert p_value == pytest.approx(0.00040695, rel=0.001) - assert p_value < 0.05, "statistically significant result" - assert stat == pytest.approx(-3.5355, rel=0.001) - - -def test_proportions_ztest_exact_match(): - stat, p_value = proportions_ztest(7, 10, 0.7) - assert p_value == 1.0, "statistically insignificant result" - assert stat == 0 - - -def test_proportions_ztest_significantly_better(): - stat, p_value = proportions_ztest(9, 10, 0.7) - assert p_value < 0.05, "statistically significant improvement" - assert proportions_ztest(9, 10, 0.7, alternative="larger")[1] < 0.05, ( - "statistically proportion is larger than expected value" - ) - assert proportions_ztest(9, 10, 0.7, alternative="two-sided")[1] < 0.05, ( - "statistically proportion is larger or smaller than expected value" - ) - - -def test_proportions_ztest_not_statistically_significantly(): - for count in range(4, 8): - stat, p_value = proportions_ztest(count, 10, 0.7) - assert p_value > 0.05, "NO statistically significant deviation" - - -def test_proportions_ztest_significantly_worse(): - stat, p_value = proportions_ztest(3, 10, 0.7) - assert p_value < 0.05, "statistically significant result" - assert proportions_ztest(3, 10, 0.7, alternative="smaller")[1] < 0.05, ( - "statistically proportion is smaller than expected value" - ) - assert proportions_ztest(3, 10, 0.7, alternative="two-sided")[1] < 0.05, ( - "statistically proportion is smaller than expected value" - ) - - -def calculate_p_value(success, failure, sample_size) -> float: - return calculate_ztest(success, failure, sample_size)[1] - - -def calculate_ztest(success, failure, sample_size) -> tuple[float, float]: - measurements = [int(success * sample_size), sample_size - failure] - samples = [sample_size, sample_size] - zstat, p_value = proportions_ztest(measurements, samples) - return zstat, p_value - - -def is_statistically_significant(success, failure, sample_size): - return calculate_p_value(success, failure, sample_size) < 0.05 - - -def test_not_is_statistically_significant(): - assert not is_statistically_significant(0.7, 3, 10), "same proportion" - assert not is_statistically_significant(0.9, 10, 100), "same proportion" - assert not is_statistically_significant(0.7, 30, 100), "same proportion" - assert not is_statistically_significant(0.7, 0, 10), "covers 100% success rate" - - -def test_is_statistically_significant(): - assert is_statistically_significant(0.9, 0, 100), "0 out of 100 > 90% success rate" - assert is_statistically_significant(0.7, 0, 11), "0 out of 11 > 70% success rate" - assert is_statistically_significant(0.9, 0, 31), "0 out of 31 > 90% success rate" - assert is_statistically_significant(0.909090, 0, 33), "0 out of 33 > 90.9% success rate" - - -def test_is_statistically_significant_with_next_success_rate(): - sample_size = 10 - assert not is_statistically_significant(next_success_rate(sample_size), 0, sample_size) - assert is_statistically_significant(next_success_rate(sample_size), 0, 34) - assert is_statistically_significant(next_success_rate(35), 0, 109) - - -def test_compare_is_within_expected_and_is_statistically_significant(): - assert is_within_expected(0.7, 3, 10), "not significant result for 3/10=70%" - assert not is_statistically_significant(0.7, 3, 10), "not significant for 3/10=70%" - - assert is_within_expected(0.7, 0, 3), "not significant result for 0 out of 3" - assert not is_statistically_significant(0.7, 0, 3), "not significant result for 0 out of 3" - - -def test_improvement_from_70_percent(): - assert is_within_expected(0.7, 0, 3), "no improvement detected at 3" - assert not is_statistically_significant(0.7, 0, 10), "no improvement detected at 10" - - assert not is_within_expected(0.7, 0, 4), "improvement detected at 4" - assert is_statistically_significant(0.7, 0, 11), "improvement detected at 11" - - -def test_improvement_from_97_percent(): - assert is_within_expected(0.97, 0, 66), "no improvement detected at 66" - assert not is_statistically_significant(0.97, 0, 100), "no improvement detected at 100" - - assert not is_within_expected(0.97, 0, 67), "significantly better at 67" - assert is_statistically_significant(0.97, 0, 101), "significantly better at 101" diff --git a/pyproject.toml b/pyproject.toml index e90f958..1d4ab31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ test = [ "pytest-asyncio>=0.21.0,<0.22", "mypy>=1.8.0,<2", "pytest-snapshot>=0.9.0", - "statsmodels>=0.14.4", ] examples = ["openai>=1.63.2,<2", "python-dotenv>=1.0.1,<2"] dev = [ diff --git a/uv.lock b/uv.lock index 4edc6a3..f30538d 100644 --- a/uv.lock +++ b/uv.lock @@ -189,7 +189,6 @@ test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-snapshot" }, - { name = "statsmodels" }, ] [package.metadata] @@ -215,7 +214,6 @@ test = [ { name = "pytest", specifier = ">=8.3.4,<9" }, { name = "pytest-asyncio", specifier = ">=0.21.0,<0.22" }, { name = "pytest-snapshot", specifier = ">=0.9.0" }, - { name = "statsmodels", specifier = ">=0.14.4" }, ] [[package]] @@ -1246,33 +1244,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] -[[package]] -name = "pandas" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, - { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, - { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, - { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, - { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, - { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, - { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, - { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, - { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, - { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, - { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, - { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, - { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, -] - [[package]] name = "pandocfilters" version = "1.5.1" @@ -1291,18 +1262,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] -[[package]] -name = "patsy" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/81/74f6a65b848ffd16c18f920620ce999fe45fe27f01ab3911260ce4ed85e4/patsy-1.0.1.tar.gz", hash = "sha256:e786a9391eec818c054e359b737bbce692f051aee4c661f4141cc88fb459c0c4", size = 396010 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/2b/b50d3d08ea0fc419c183a84210571eba005328efa62b6b98bc28e9ead32a/patsy-1.0.1-py2.py3-none-any.whl", hash = "sha256:751fb38f9e97e62312e921a1954b81e1bb2bcda4f5eeabaf94db251ee791509c", size = 232923 }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -1636,15 +1595,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/72/2f30cf26664fcfa0bd8ec5ee62ec90c03bd485e4a294d92aabc76c5203a5/python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090", size = 14924 }, ] -[[package]] -name = "pytz" -version = "2025.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, -] - [[package]] name = "pywin32" version = "308" @@ -1843,35 +1793,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, ] -[[package]] -name = "scipy" -version = "1.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/40/09319f6e0f276ea2754196185f95cd191cb852288440ce035d5c3a931ea2/scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf", size = 38717587 }, - { url = "https://files.pythonhosted.org/packages/fe/c3/2854f40ecd19585d65afaef601e5e1f8dbf6758b2f95b5ea93d38655a2c6/scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37", size = 30100266 }, - { url = "https://files.pythonhosted.org/packages/dd/b1/f9fe6e3c828cb5930b5fe74cb479de5f3d66d682fa8adb77249acaf545b8/scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d", size = 22373768 }, - { url = "https://files.pythonhosted.org/packages/15/9d/a60db8c795700414c3f681908a2b911e031e024d93214f2d23c6dae174ab/scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb", size = 25154719 }, - { url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 }, - { url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 }, - { url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 }, - { url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 }, - { url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 }, - { url = "https://files.pythonhosted.org/packages/4c/4b/a57f8ddcf48e129e6054fa9899a2a86d1fc6b07a0e15c7eebff7ca94533f/scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9", size = 38870286 }, - { url = "https://files.pythonhosted.org/packages/0c/43/c304d69a56c91ad5f188c0714f6a97b9c1fed93128c691148621274a3a68/scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f", size = 30141634 }, - { url = "https://files.pythonhosted.org/packages/44/1a/6c21b45d2548eb73be9b9bff421aaaa7e85e22c1f9b3bc44b23485dfce0a/scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6", size = 22415179 }, - { url = "https://files.pythonhosted.org/packages/74/4b/aefac4bba80ef815b64f55da06f62f92be5d03b467f2ce3668071799429a/scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af", size = 25126412 }, - { url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 }, - { url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 }, - { url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 }, - { url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 }, - { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 }, -] - [[package]] name = "send2trash" version = "1.8.3" @@ -2062,27 +1983,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] -[[package]] -name = "statsmodels" -version = "0.14.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "patsy" }, - { name = "scipy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/3b/963a015dd8ea17e10c7b0e2f14d7c4daec903baf60a017e756b57953a4bf/statsmodels-0.14.4.tar.gz", hash = "sha256:5d69e0f39060dc72c067f9bb6e8033b6dccdb0bae101d76a7ef0bcc94e898b67", size = 20354802 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/f8/2662e6a101315ad336f75168fa9bac71f913ebcb92a6be84031d84a0f21f/statsmodels-0.14.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5a24f5d2c22852d807d2b42daf3a61740820b28d8381daaf59dcb7055bf1a79", size = 10186886 }, - { url = "https://files.pythonhosted.org/packages/fa/c0/ee6e8ed35fc1ca9c7538c592f4974547bf72274bc98db1ae4a6e87481a83/statsmodels-0.14.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df4f7864606fa843d7e7c0e6af288f034a2160dba14e6ccc09020a3cf67cb092", size = 9880066 }, - { url = "https://files.pythonhosted.org/packages/d1/97/3380ca6d8fd66cfb3d12941e472642f26e781a311c355a4e97aab2ed0216/statsmodels-0.14.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91341cbde9e8bea5fb419a76e09114e221567d03f34ca26e6d67ae2c27d8fe3c", size = 10283521 }, - { url = "https://files.pythonhosted.org/packages/fe/2a/55c5b5c5e5124a202ea3fe0bcdbdeceaf91b4ec6164b8434acb9dd97409c/statsmodels-0.14.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1322286a7bfdde2790bf72d29698a1b76c20b8423a55bdcd0d457969d0041f72", size = 10723228 }, - { url = "https://files.pythonhosted.org/packages/4f/76/67747e49dc758daae06f33aad8247b718cd7d224f091d2cd552681215bb2/statsmodels-0.14.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e31b95ac603415887c9f0d344cb523889cf779bc52d68e27e2d23c358958fec7", size = 10859503 }, - { url = "https://files.pythonhosted.org/packages/1d/eb/cb8b01f5edf8f135eb3d0553d159db113a35b2948d0e51eeb735e7ae09ea/statsmodels-0.14.4-cp313-cp313-win_amd64.whl", hash = "sha256:81030108d27aecc7995cac05aa280cf8c6025f6a6119894eef648997936c2dd0", size = 9817574 }, -] - [[package]] name = "tabulate" version = "0.9.0" @@ -2175,15 +2075,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] -[[package]] -name = "tzdata" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, -] - [[package]] name = "uri-template" version = "1.3.0"