diff --git a/pydeflate/core/api.py b/pydeflate/core/api.py index 2c8c227..ec372d7 100644 --- a/pydeflate/core/api.py +++ b/pydeflate/core/api.py @@ -318,9 +318,9 @@ def _calculate_deflator_value( pd.Series: Series with combined deflator values. """ return ( - (10_000 * exchange_rate) / (price_def * exchange_def) + (exchange_def * exchange_rate) / price_def if self.to_current - else (price_def * exchange_def) / (10_000 * exchange_rate) + else price_def / (exchange_def * exchange_rate) ) def _merge_components(self, df: pd.DataFrame, other: pd.DataFrame): diff --git a/pydeflate/core/exchange.py b/pydeflate/core/exchange.py index 190f56a..a01e596 100644 --- a/pydeflate/core/exchange.py +++ b/pydeflate/core/exchange.py @@ -112,17 +112,17 @@ def exchange_rate(self, from_currency: str, to_currency: str): pydeflate_EXCHANGE=lambda d: d.pydeflate_EXCHANGE / d.pydeflate_EXCHANGE_to ) - # Drop unnecessary columns - merged = merged.drop(columns=merged.filter(regex="_to$").columns, axis=1) - # Compute the exchange rate deflator merged = compute_exchange_deflator( merged, - exchange="pydeflate_EXCHANGE", + exchange="pydeflate_EXCHANGE_to", year="pydeflate_year", grouper=["pydeflate_entity_code", "pydeflate_iso3"], ) + # Drop unnecessary columns + merged = merged.drop(columns=merged.filter(regex="_to$").columns, axis=1) + return merged def exchange( diff --git a/pydeflate/sources/common.py b/pydeflate/sources/common.py index 9b95c00..33c900f 100644 --- a/pydeflate/sources/common.py +++ b/pydeflate/sources/common.py @@ -207,6 +207,13 @@ def _add_deflator( exchange: str = "EXCHANGE", year: str = "year", ) -> pd.DataFrame: + + # if needed, clean exchange name + if exchange.endswith("_to") or exchange.endswith("_from"): + exchange_name = exchange.rsplit("_", 1)[0] + else: + exchange_name = exchange + # Identify the base year for the deflator if measure is not None: base_year = identify_base_year(group, measure=measure, year=year) @@ -222,7 +229,9 @@ def _add_deflator( # If base value is found and valid, calculate the deflator if base_value.size > 0 and pd.notna(base_value[0]): - group[f"{exchange}_D"] = round(100 * group[exchange] / base_value[0], 6) + group[f"{exchange_name}_D"] = round( + 100 * group[exchange] / base_value[0], 6 + ) return group diff --git a/tests/test_dac_totals.py b/tests/test_dac_totals.py new file mode 100644 index 0000000..167c695 --- /dev/null +++ b/tests/test_dac_totals.py @@ -0,0 +1,146 @@ +import pandas as pd + +from pydeflate import oecd_dac_deflate + +data = { + "year": [2020, 2021, 2022, 2023], + "indicator": ["total_oda_official_definition"] * 4, + "donor_code": [12] * 4, + "currency": ["USD"] * 4, + "prices": ["current"] * 4, + "value": [18568.19, 15712.01, 15761.81, 19110.59], + "expected_value": [16312, 12885, 13717, 15374], +} + +df = pd.DataFrame(data) + +data_usd = { + "year": [2020, 2021, 2022, 2023], + "indicator": ["total_oda_official_definition"] * 4, + "donor_code": [302] * 4, + "currency": ["USD"] * 4, + "prices": ["current"] * 4, + "value": [35576.31, 47804.8, 60522.41, 66040.03], + "expected_value": [41326, 53097, 62800, 66040], +} + + +df_usd = pd.DataFrame(data_usd) + +data_eur = { + "year": [2020, 2021, 2022, 2023], + "indicator": ["total_oda_official_definition"] * 4, + "donor_code": [742] * 4, + "currency": ["EUR"] * 4, + "prices": ["current"] * 4, + "value": [1974, 2429, 2672, 2986], + "expected_value": [1995, 2403, 2619, 2896], +} +df_eur = pd.DataFrame(data_eur) + + +data_can = { + "year": [2020, 2021, 2022, 2023], + "indicator": ["total_oda_official_definition"] * 4, + "donor_code": [5] * 4, + "currency": ["CAD"] * 4, + "prices": ["current"] * 4, + "value": [38503, 41707, 46393, 49495], + "expected_value": [31404, 34049, 38957, 36682], +} +df_can = pd.DataFrame(data_can) + + +data_lcu = { + "year": [2020, 2021, 2022, 2023], + "indicator": ["total_oda_official_definition"] * 4, + "donor_code": [4] * 4, + "currency": ["EUR"] * 4, + "prices": ["current"] * 4, + "value": [12394, 13112, 15228, 14266], + "expected_value": [13625, 14210, 16031, 14266], +} +df_lcu = pd.DataFrame(data_lcu) + + +def run_constant_test( + data, + source_currency, + target_currency, + tolerance=0.05, + base_year=2023, + id_column="donor_code", + target_value_column="value", +): + """ + Runs a test for deflation calculation with given parameters and tolerance. + + Args: + data (pd.DataFrame): The input DataFrame containing the data to deflate. + source_currency (str): The source currency code. + target_currency (str): The target currency code. + tolerance (float, optional): The allowed tolerance for deviation. Defaults to 0.05. + base_year (int, optional): The base year for deflation. Defaults to 2023. + id_column (str, optional): Column name for IDs. Defaults to "donor_code". + target_value_column (str, optional): Column name for the target value. Defaults to "value". + + Raises: + AssertionError: If any row exceeds the tolerance threshold. + """ + # Perform the deflation calculation + test_df = oecd_dac_deflate( + data=data, + base_year=base_year, + source_currency=source_currency, + target_currency=target_currency, + id_column=id_column, + use_source_codes=True, + target_value_column=target_value_column, + ) + + # Calculate the percentage deviation + deviations = abs( + (test_df[target_value_column] - test_df["expected_value"]) + / test_df["expected_value"] + ) + + # Filter out rows where value is NaN and deviations exceed tolerance + mask = test_df[target_value_column].notna() & (deviations >= tolerance) + failing_rows = test_df[mask] + + # Assert that no rows exceed the tolerance + assert failing_rows.empty, ( + f"Deviation exceeded {tolerance*100:.2f}% in the following " + f"donors:\n{failing_rows[id_column].unique()}" + ) + + +# Define test cases with parameters +def test_to_constant(): + run_constant_test( + data=df, source_currency="USA", target_currency="GBP", tolerance=0.01 + ) + + +def test_to_constant_usd(): + run_constant_test( + data=df_usd, source_currency="USA", target_currency="USA", tolerance=0.01 + ) + + +def test_to_constant_eur(): + run_constant_test( + data=df_eur, source_currency="EUR", target_currency="EUR", tolerance=0.05 + ) + + +def test_to_constant_can(): + run_constant_test( + data=df_can, source_currency="CAN", target_currency="USA", tolerance=0.05 + ) + + +def test_to_constant_lcu(): + run_constant_test( + data=df_lcu, source_currency="EUR", target_currency="LCU", tolerance=0.05 + )