pydeflate 1.4.1__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,78 +0,0 @@
1
- """Defines the deflator class"""
2
-
3
- from dataclasses import dataclass
4
-
5
- import numpy as np
6
- import pandas as pd
7
-
8
- from pydeflate.get_data.deflate_data import Data
9
- from pydeflate.get_data.exchange_data import Exchange
10
-
11
-
12
- @dataclass
13
- class Deflator:
14
- base_year: int
15
- exchange_obj: Exchange
16
- deflator_obj: Data
17
- deflator_method: str
18
- source_currency: str = "LCU"
19
- target_currency: str = "USA"
20
- to_current: bool = False
21
- _xe_df: pd.DataFrame | None = None
22
- _xe_deflator_df: pd.DataFrame | None = None
23
- _price_deflator_df: pd.DataFrame | None = None
24
- """A class to produce deflators based on the given parameters.
25
-
26
- Deflators take into account changes in prices and the evolution of currency exchange
27
- values. They compensate for both factors in order to produce a standard unit of
28
- measurement, regardless of the year of the flow.
29
-
30
- Args:
31
- base_year: the year in which the deflator equals 100 (if the source and target
32
- currency are equal).
33
- Exchange: the exchange rate object.
34
- price: the price data oject.
35
- source_currency: the ISO3 code of the country which uses the source currency
36
- target_currency: the ISO3 code of the country which used the target currency
37
- _deflator_df: the resulting DataFrame containing the deflators data.
38
- """
39
-
40
- def __post_init__(self):
41
- # Exchange deflator DataFrame, including any currency conversion
42
- self._xe_deflator_df = self.exchange_obj.exchange_deflator(
43
- source_iso=self.source_currency,
44
- target_iso=self.target_currency,
45
- base_year=self.base_year,
46
- )
47
-
48
- # Price deflator DataFrame
49
- self._price_deflator_df = self.deflator_obj.get_deflator(
50
- base_year=self.base_year, method=self.deflator_method
51
- )
52
-
53
- def get_deflator(self) -> pd.DataFrame:
54
- """Get the deflator DataFrame"""
55
-
56
- # Merge the price and currency exchange deflators
57
- df = self._price_deflator_df.merge(
58
- self._xe_deflator_df,
59
- on=["iso_code", "year"],
60
- how="left",
61
- suffixes=("_def", "_xe"),
62
- )
63
-
64
- # Calculate the deflator
65
- df = (
66
- df.assign(
67
- value=round(100 * df.value_def / df.value_xe, 6)
68
- if not self.to_current
69
- else round(100 * df.value_xe / df.value_def, 6)
70
- )
71
- .filter(["iso_code", "year", "value"], axis=1)
72
- .assign(value=lambda d: d.value.replace(0, np.nan))
73
- .dropna(subset=["value"])
74
- .reset_index(drop=True)
75
- .rename(columns={"value": "deflator"})
76
- )
77
-
78
- return df
@@ -1,70 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
- from dataclasses import dataclass
5
-
6
- import pandas as pd
7
-
8
-
9
- @dataclass
10
- class Data(ABC):
11
- _data: pd.DataFrame | None = None
12
- _available_methods: dict | None = None
13
- """Abstract class defining the basic structure and functionality of Data classes.
14
-
15
- Data classes store the price data from different sources.
16
- """
17
-
18
- @abstractmethod
19
- def update(self, **kwargs) -> None:
20
- """Update underlying data"""
21
-
22
- @abstractmethod
23
- def load_data(self, **kwargs) -> None:
24
- """Load required data to construct deflator"""
25
-
26
- def available_methods(self) -> dict:
27
- """Return a dictionary of available methods with their functions"""
28
- return self._available_methods
29
-
30
- def get_method(self, method: str) -> pd.DataFrame:
31
- """Return the data for a given method"""
32
- if self._data is None:
33
- self.load_data()
34
-
35
- if method not in self._data.indicator.unique():
36
- raise ValueError(f"Invalid method {method=}")
37
-
38
- return (
39
- self._data.loc[lambda d: d.indicator == method]
40
- .filter(["iso_code", "year", "value"], axis=1)
41
- .sort_values(["iso_code", "year"])
42
- .reset_index(drop=True)
43
- )
44
-
45
- def get_deflator(self, base_year: int, method: str | None = None) -> pd.DataFrame:
46
- if method is None:
47
- raise ValueError("method must be specified")
48
-
49
- if method not in self.available_methods():
50
- raise ValueError(f"method must be one of {self.available_methods().keys()}")
51
-
52
- if self._data is None:
53
- self.load_data()
54
-
55
- d_ = self.get_method(self._available_methods[method])
56
-
57
- # get the data for the selected base year.
58
- d_base = d_.query(f"year.dt.year == {base_year}")
59
-
60
- # If base year is not valid, raise error
61
- if len(d_base) == 0:
62
- raise ValueError(f"No currency exchange data for {base_year=}")
63
-
64
- # Merge the exchange data and the base year data
65
- d_ = d_.merge(d_base, how="left", on=["iso_code"], suffixes=("", "_base"))
66
-
67
- # Calculate the deflator
68
- d_.value = round(100 * d_.value / d_.value_base, 6)
69
-
70
- return d_.filter(["iso_code", "year", "value"], axis=1)
@@ -1,371 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from dataclasses import dataclass
3
-
4
- import pandas as pd
5
-
6
- from pydeflate.get_data.imf_data import IMF
7
- from pydeflate.get_data.wb_data import END, START, update_world_bank_data
8
- from pydeflate.pydeflate_config import PYDEFLATE_PATHS, logger
9
- from pydeflate.utils import emu
10
-
11
-
12
- def _exchange_ratio(source_xe: pd.DataFrame, target_xe: pd.DataFrame) -> pd.DataFrame:
13
- return (
14
- source_xe.merge(
15
- target_xe,
16
- how="left",
17
- on=["year", "iso_code"],
18
- suffixes=("_source", "_target"),
19
- )
20
- .assign(value=lambda d: d.value_source / d.value_target)
21
- .drop(["value_source", "value_target"], axis=1)
22
- )
23
-
24
-
25
- def _calculate_deflator(xe: pd.DataFrame, base_year: int) -> pd.DataFrame:
26
- # get the data for the selected base year.
27
- xe_base = xe.query(f"year.dt.year == {base_year}")
28
-
29
- # If base year is not valid, raise error
30
- if len(xe_base) == 0:
31
- raise ValueError(f"No currency exchange data for {base_year=}")
32
-
33
- # Merge the exchange data and the base year data
34
- xe = xe.merge(xe_base, how="left", on=["iso_code"], suffixes=("", "_base"))
35
-
36
- # Calculate the deflator
37
- xe.value = round(100 * xe.value / xe.value_base, 6)
38
-
39
- return xe.filter(["iso_code", "year", "value"], axis=1)
40
-
41
-
42
- @dataclass
43
- class Exchange(ABC):
44
- """An abstract class to update, load and return exchange rate data.
45
-
46
- Attributes:
47
- method: the method to use to calculate the exchange rate.
48
-
49
- """
50
-
51
- method: str | None
52
- _data: pd.DataFrame | None = None
53
-
54
- @abstractmethod
55
- def update(self, **kwargs) -> None:
56
- """Update underlying data"""
57
-
58
- @abstractmethod
59
- def load_data(self, **kwargs) -> None:
60
- """Load required data to construct deflator"""
61
-
62
- @abstractmethod
63
- def usd_exchange_rate(self, direction: str = "lcu_to_uds") -> pd.DataFrame:
64
- """Get the exchange rate of a currency to USD (or vice versa)
65
-
66
- Args:
67
- direction: the direction of the exchange rate. Either "lcu_to_usd"
68
- or "usd_to_lcu".
69
-
70
- Returns:
71
- A dataframe with the exchange rate of the currency to USD (or vice versa).
72
- """
73
-
74
- def exchange_rate(self, currency_iso: str) -> pd.DataFrame:
75
- """Get an exchange rate for a given ISO
76
-
77
- Args:
78
- currency_iso: the iso_code of the currency to get the exchange rate for.
79
-
80
- Returns:
81
- A dataframe with the exchange rate for the specified currency.
82
- """
83
-
84
- # get usd exchange rates
85
- d_ = self.usd_exchange_rate()
86
-
87
- # filter the exchange rate of the chosen currency
88
- target_iso = d_.query(f"iso_code == '{currency_iso}'")
89
-
90
- # If currency is not valid, raise error
91
- if len(target_iso) == 0:
92
- raise ValueError(f"No currency exchange data for {currency_iso=}")
93
-
94
- # merge the two datasets and calculate new exchange rate
95
- return (
96
- pd.merge(d_, target_iso, how="left", on=["year"], suffixes=("", "_xe"))
97
- .assign(value=lambda d: d.value / d.value_xe)
98
- .drop(columns=["value_xe", "iso_code_xe"])
99
- )
100
-
101
- def exchange_deflator(
102
- self, source_iso: str, target_iso: str, base_year: int
103
- ) -> pd.DataFrame:
104
- """Get an exchange rate deflator for a given ISO
105
-
106
- Args:
107
- source_iso: the iso_code in which the "original" data is based.
108
- The default should be "LCU".
109
- target_iso: the iso_code of the currency to get the exchange rate for.
110
- base_year: the base year to calculate the deflator.
111
-
112
- Returns:
113
- A dataframe with the exchange rate deflator for the specified currency.
114
-
115
- """
116
- # Get exchange data based on the source currency. If LCU set to None
117
- source_xe = (
118
- self.exchange_rate(currency_iso=source_iso)
119
- if source_iso != "LCU"
120
- else self.exchange_rate(currency_iso="USA").assign(value=1)
121
- )
122
-
123
- # Get exchange data based on the target currency. If LCU set to None
124
- target_xe = (
125
- self.exchange_rate(currency_iso=target_iso)
126
- if target_iso != "LCU"
127
- else self.exchange_rate(currency_iso="USA").assign(value=1)
128
- )
129
-
130
- # calculate conversion ratio
131
- exchange_ratio = _exchange_ratio(source_xe=source_xe, target_xe=target_xe)
132
-
133
- # get the data for the selected base year.
134
- if target_iso != "LCU":
135
- xe = self.exchange_rate(currency_iso=target_iso)
136
- elif source_iso != "LCU":
137
- xe = self.exchange_rate(currency_iso=source_iso)
138
- else:
139
- xe = self.exchange_rate(currency_iso="USA").assign(value=1)
140
-
141
- # Calculate the deflator
142
- xe = _calculate_deflator(xe=xe, base_year=base_year)
143
-
144
- # Merge the conversion ratio
145
- xe = xe.merge(
146
- exchange_ratio,
147
- how="left",
148
- on=["year", "iso_code"],
149
- suffixes=("_xe", "_xe_ratio"),
150
- )
151
-
152
- # Convert deflators to currency of interest
153
- xe = xe.assign(value=lambda d: d.value_xe * d.value_xe_ratio)
154
-
155
- return xe.filter(["year", "iso_code", "value"], axis=1)
156
-
157
-
158
- @dataclass
159
- class ExchangeOECD(Exchange):
160
- """The OECD exchange rate data
161
-
162
- Attributes:
163
- method: the method to use to calculate the exchange rate. For this source,
164
- the only valid method is “implied”.
165
- """
166
-
167
- method: str = "implied"
168
- _load_try_count: int = 0
169
-
170
- @staticmethod
171
- def update(**kwargs) -> None:
172
- """Update the DAC1 data, which is the source for the OECD exchange rates."""
173
- from pydeflate.get_data import oecd_data
174
-
175
- oecd_data.update_dac1()
176
-
177
- def load_data(self, **kwargs) -> None:
178
- """Load the OECD DAC price deflators data.
179
-
180
- If the data is not found, it will be downloaded.
181
- DAC deflators are transformed into price deflators by using the
182
- implied exchange rate information from the OECD DAC data.
183
-
184
- The deflators that are loaded is therefore *not* the DAC deflator,
185
- but the price deflator used to produce the DAC deflators.
186
-
187
- """
188
- try:
189
- self._data = pd.read_feather(
190
- PYDEFLATE_PATHS.data / "pydeflate_dac1.feather"
191
- )
192
- except FileNotFoundError:
193
- logger.info("Data not found, downloading...")
194
- self.update()
195
- self.load_data()
196
- return
197
-
198
- def usd_exchange_rate(self, direction: str = "lcu_usd") -> pd.DataFrame:
199
- """Get the exchange rate of a currency to USD (or vice versa)
200
-
201
- Args:
202
- direction: the direction of the exchange rate. Either "lcu_to_usd"
203
- or "usd_to_lcu".
204
-
205
- Returns:
206
- A dataframe with the exchange rate of the currency to USD (or vice versa).
207
- """
208
-
209
- # If data is not loaded, load it
210
- if self._data is None:
211
- self.load_data()
212
-
213
- # Filter the data to only include the required columns, rename the value column.
214
- d_ = self._data[["iso_code", "year", "exchange"]].rename(
215
- columns={"exchange": "value"}
216
- )
217
-
218
- # if the direction is lcu to usd, return the data
219
- if direction == "lcu_usd":
220
- return d_
221
- # if the direction is usd to lcu, invert the exchange rate.
222
- elif direction == "usd_lcu":
223
- d_.value = 1 / d_.value
224
- return d_
225
- # if the direction is not valid, raise error
226
- else:
227
- raise ValueError("Direction must be 'lcu_usd' or 'usd_lcu'")
228
-
229
-
230
- @dataclass
231
- class ExchangeWorldBank(Exchange):
232
- """The World Bank exchange rate data
233
-
234
- Attributes:
235
- method: the method to use to calculate the exchange rate. For this source,
236
- the valid methods are "yearly_average" and "effective_exchange".
237
- """
238
-
239
- method: str = "yearly_average"
240
- _load_try_count: int = 0
241
-
242
- def __post_init__(self):
243
- """Check that the method is valid"""
244
- if self.method not in ["yearly_average", "effective_exchange"]:
245
- raise ValueError("Method must be 'yearly_average' or 'effective_exchange'")
246
-
247
- def update(self) -> None:
248
- """Update the World Bank data"""
249
- update_world_bank_data()
250
-
251
- def load_data(self, **kwargs) -> None:
252
- """Load the World Bank data"""
253
- # method mapping
254
- method_map = {
255
- "yearly_average": "PA.NUS.FCRF",
256
- "effective_exchange": "PX.REX.REER",
257
- }
258
-
259
- if self._load_try_count < 1:
260
- # Try to load the data from disk
261
- try:
262
- self._data = (
263
- pd.read_csv(
264
- PYDEFLATE_PATHS.data
265
- / f"{method_map[self.method]}_{START}-{END}_.csv",
266
- parse_dates=["date"],
267
- )
268
- .rename(columns={"date": "year"})
269
- .drop(columns=["indicator_code"])
270
- )
271
-
272
- # If the data is not found, download it. Reload the data to the object.
273
- except FileNotFoundError:
274
- logger.info("World Bank data not found, downloading...")
275
- self._load_try_count = +1
276
- self.update()
277
- self.load_data()
278
- else:
279
- raise FileNotFoundError("Could not load World Bank data")
280
-
281
- def usd_exchange_rate(self, direction: str = "lcu_usd") -> pd.DataFrame:
282
- """Get the exchange rate of a currency to USD (or vice versa)
283
-
284
- Args:
285
- direction: the direction of the exchange rate. Either "lcu_to_usd"
286
- or "usd_to_lcu".
287
-
288
- Returns:
289
- A dataframe with the exchange rate of the currency to USD (or vice versa).
290
-
291
- """
292
-
293
- if direction not in ["lcu_usd", "usd_lcu"]:
294
- raise ValueError("Direction must be 'lcu_usd' or 'usd_lcu'")
295
-
296
- # If data is not loaded, load it
297
- if self._data is None:
298
- self.load_data()
299
-
300
- # Load the data into a temporary variable
301
- df = self._data.copy(deep=True)
302
-
303
- # Find the "Euro" data. This is done given that some countries are missing
304
- # exchange rates, but they are part of the Euro area.
305
- eur = df.loc[df.iso_code == "EMU"].dropna().set_index("year")["value"].to_dict()
306
-
307
- # Euro area countries without exchange rates
308
- eur_mask = (df.iso_code.isin(emu())) & (df.value.isna())
309
-
310
- # Assign EURO exchange rate to euro are countries from year euro adopted
311
- df.loc[eur_mask, "value"] = df.year.map(eur)
312
-
313
- # If the direction is lcu to usd, return the data
314
-
315
- if direction == "lcu_usd":
316
- return df
317
- # if the direction is usd to lcu, invert the exchange rate.
318
-
319
- df.value = 1 / df.value
320
- return df
321
-
322
-
323
- @dataclass
324
- class ExchangeIMF(Exchange):
325
- """The IMF exchange rate data
326
-
327
- Attributes:
328
- method: the method to use to calculate the exchange rate. For this source,
329
- the only valid method is “implied”.
330
-
331
- """
332
-
333
- method: str = "implied"
334
- _imf: IMF | None = None
335
-
336
- def __post_init__(self):
337
- """Load an IMF object to manage getting the data"""
338
- if self._imf is None:
339
- self._imf = IMF()
340
-
341
- def update(self, **kwargs) -> None:
342
- """Update the IMF data"""
343
- self._imf.update()
344
-
345
- def load_data(self, **kwargs) -> None:
346
- """Load the IMF data"""
347
-
348
- # if no direction is passed, default to lcu to usd
349
- direction = "lcu_usd" if "direction" not in kwargs else kwargs["direction"]
350
- self._data = self._imf.implied_exchange(direction=direction)
351
-
352
- def usd_exchange_rate(self, direction: str = "lcu_usd") -> pd.DataFrame:
353
- """Get the exchange rate of a currency to USD (or vice versa)
354
-
355
- Args:
356
- direction: the direction of the exchange rate. Either "lcu_to_usd"
357
- or "usd_to_lcu".
358
-
359
- Returns:
360
- A dataframe with the exchange rate of the currency to USD (or vice versa).
361
-
362
- """
363
-
364
- if direction not in ["lcu_usd", "usd_lcu"]:
365
- raise ValueError("Direction must be 'lcu_usd' or 'usd_lcu'")
366
-
367
- # load the data
368
- self.load_data(direction=direction)
369
-
370
- # return the data
371
- return self._data
@@ -1,76 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
- import pandas as pd
4
- from bblocks import WorldEconomicOutlook, set_bblocks_data_path
5
-
6
- from pydeflate.get_data.deflate_data import Data
7
- from pydeflate.pydeflate_config import PYDEFLATE_PATHS
8
- from pydeflate.tools.update_data import update_update_date
9
-
10
- set_bblocks_data_path(PYDEFLATE_PATHS.data)
11
-
12
-
13
- @dataclass
14
- class IMF(Data):
15
- _weo: WorldEconomicOutlook = None
16
- """An object to download and return the latest IMF WEO data for several indicators"""
17
-
18
- def __post_init__(self):
19
- self._weo = WorldEconomicOutlook()
20
- self._available_methods = {
21
- "Consumer price index": "PCPI",
22
- "Consumer price index, end of period": "PCPIE",
23
- "Gross domestic product, deflator": "NGDP_D",
24
- "gdp": "NGDP_D",
25
- "pcpi": "PCPI",
26
- "pcpie": "PCPIE",
27
- }
28
-
29
- def update(self) -> None:
30
- """Update the stored WEO data, using WEO package."""
31
- self._weo.update_data(reload_data=False, year=None, release=None)
32
- update_update_date(source="IMF")
33
-
34
- def load_data(
35
- self, latest_y: int | None = None, latest_r: int | None = None
36
- ) -> None:
37
- """Loading WEO as a clean dataframe
38
-
39
- Args:
40
- latest_y: passed only optional to override the behaviour to get the latest
41
- release year for the WEO.
42
- latest_r: passed only optionally to override the behaviour to get the latest
43
- released value (1 or 2).
44
- """
45
- # load the data for available indicators
46
- indicators = list(self._available_methods.values()) + ["NGDP", "NGDPD"]
47
- self._weo.load_data(indicators)
48
-
49
- # get the data into a dataframe
50
- self._data = self._weo.get_data(keep_metadata=False)
51
-
52
- def implied_exchange(self, direction: str = "lcu_usd") -> pd.DataFrame:
53
- """Get the implied exchange rate used by the IMF
54
- Args:
55
- direction: the direction of the exchange rate, either lcu_usd for
56
- local currency units per usd, or usd_lcu for the opposite.
57
- """
58
-
59
- # USD data
60
- usd = self.get_method("NGDPD")
61
-
62
- # LCU data
63
- lcu = self.get_method("NGDP")
64
-
65
- # Merge datasets
66
- d_ = usd.merge(lcu, on=["iso_code", "year"], suffixes=("_usd", "_lcu"))
67
-
68
- # Calculate implied exchange rate
69
- if direction == "lcu_usd":
70
- d_["value"] = d_["value_lcu"] / d_["value_usd"]
71
- elif direction == "usd_lcu":
72
- d_["value"] = d_["value_usd"] / d_["value_lcu"]
73
- else:
74
- raise ValueError(f"direction must be lcu_usd or usd_lcu, not {direction}")
75
-
76
- return d_.filter(["iso_code", "year", "value"], axis=1)