Source code for easymoney.money

# coding: utf-8

"""

    Main API Functionality
    ~~~~~~~~~~~~~~~~~~~~~~

"""
# Imports
import pycountry
import numpy as np
import pandas as pd

from pprint import pprint
from warnings import warn
from datetime import datetime

# Support Tools
from easymoney.support_tools import mint
from easymoney.support_tools import min_max
from easymoney.support_tools import closest_date
from easymoney.support_tools import year_extract
from easymoney.support_tools import min_max_dates
from easymoney.support_tools import closest_value
from easymoney.support_tools import fast_date_range
from easymoney.support_tools import date_format_check
from easymoney.support_tools import sort_range_reverse

# Pycountry Wrap
from easymoney.pycountry_wrap import PycountryWrap

# options_tools
from easymoney.options_tools import options_ranking
from easymoney.options_tools import year_date_overlap
from easymoney.options_tools import alpha2_by_cpi_years

# Easy Pandas
from easymoney.easy_pandas import pandas_null_drop
from easymoney.easy_pandas import pandas_pretty_print

# Online Data Sources
from easymoney.sources.ecb_interface import ecb_xml_exchange_data
from easymoney.sources.world_bank_interface import world_bank_pull



[docs]class EasyPeasy(object): """ Tools for Monetary Information and Conversions. :param precision: number of places to round to when returning results. Defaults to 2. :type precision: ``int`` :param fall_back: if True, fall back to closest possible date for which data is available. Defaults to True. :type fall_back: ``bool`` :param fuzzy_match_threshold: a threshold for fuzzy matching confidence (requires the ``fuzzywuzzy`` package). The value must be an number between 0 and 100. The *suggested* minimum values is 85. This will only impact attempts to match on natural names, e.g., attempting to match 'Canada' by passing 'Canadian'. If True, a threshold of 90 will be set. Defaults to False. .. warning:: Fuzzy matching may yield inaccurate results. When possible, use terminology *exactly* as it appears in ``options()``. :type fuzzy_threshold: ``int``, ``float`` or ``bool`` :param data_path: alternative path to the database file(s). Defaults to None. :type data_path: ``str`` """ # Fix: `EasyPeasy()` does not handle currencies like 'EEK' properly. # They may not been appearing in options() correctly. # See: _user_currency_input() below. def __init__(self, precision=2, fall_back=True, fuzzy_threshold=False, data_path=None): """ Initialize the ``EasyPeasy()`` class. """ self._precision = precision self._fall_back = fall_back fuzzy_search_threshold = fuzzy_threshold recommended_fuzzy_threshold = 90 min_suggested_fuzzy_threshold = 85 # Check fuzzy_threshold if fuzzy_threshold is not False and not isinstance(fuzzy_threshold, (float, int)): raise ValueError("`fuzzy_threshold` must be either `False`, a `float` or an `int`.") elif fuzzy_threshold is True: fuzzy_search_threshold = recommended_fuzzy_threshold elif fuzzy_threshold is not False and isinstance(fuzzy_threshold, (float, int)) and fuzzy_threshold <= 0: raise ValueError("`fuzzy_threshold` must be greater than 0.") elif fuzzy_threshold is not False and isinstance(fuzzy_threshold, (float, int)) and fuzzy_threshold > 100: raise ValueError("`fuzzy_threshold` must be less than 100.") elif fuzzy_threshold is not False and isinstance(fuzzy_threshold, (float, int)) \ and fuzzy_threshold < min_suggested_fuzzy_threshold: warn("\nLow `fuzzy_threshold` values, such as %s, have an elevated " "likelihood of innaccurate results." % (str(fuzzy_threshold))) path_to_data = data_path if isinstance(data_path, str) else None self._pycountry_wrap = PycountryWrap(path_to_data, fuzzy_search_threshold) self._pycountries_alpha_2 = set([c.alpha_2 for c in list(pycountry.countries)]) # CPI Dictionaries self._cpi_dict = world_bank_pull(return_as='dict') self._alpha2_cpi_record = alpha2_by_cpi_years(regions=self._pycountries_alpha_2, cpi_dictionary=self._cpi_dict) # Exchange Dict self._exchange_dict, self._ecb_currency_codes, self._currency_date_record = ecb_xml_exchange_data(return_as='dict') # Min max for _currency_date_record self._currency_date_record_range = {k: fast_date_range(v, '%d/%m/%Y') for k, v in self._currency_date_record.items()} # Column Order for options self._table_col_order = ['RegionFull', 'Region', 'Alpha2', 'Alpha3', 'Currencies', 'InflationDates', 'ExchangeDates', 'Overlap'] def _params_check(self, amount="void", pretty_print="void"): """ Check `amount` and `pretty_print` have been passed valid values. :param amount: numeric amount :type amount: ``float`` or ``int`` :param pretty_print: must be a boolean :type pretty_print: ``bool`` """ # Check amount is float if amount != "void" and not isinstance(amount, (float, int)): raise ValueError("amount must be numeric (intiger or float).") # Check pretty_print is a bool if pretty_print != "void" and not isinstance(pretty_print, bool): raise ValueError("pretty_print must be either True or False.")
[docs] def region_map(self, region, map_to='alpha_2'): """ Map a 'region' to any one of: ISO Alpha 2, ISO Alpha 3, it's Name or Offical Name. Examples: - ``EasyPeasy().region_map(region='CA', map_to='alpha_2')`` :math:`=` 'CA' - ``EasyPeasy().region_map(region='Canada', map_to='alpha_3')`` :math:`=` 'CAN' :param region: a 'region' in the format of a ISO Alpha2, ISO Alpha3 or currency code, as well as natural name. :type region: ``str`` :param map_to: - for region: 'alpha_2', 'alpha_3', 'name' or 'official_name'. - for currency: 'currency_alpha_3', 'currency_numeric' or 'currency_name'. Defaults to 'alpha_2'. :type map_to: ``str`` :return: the desired mapping from region to ISO Alpha2. :rtype: ``str`` or ``tuple`` """ # handle currency transitions here. return self._pycountry_wrap.map_region_to_type(region=region, extract_type=map_to)
def _cpi_years(self, region, warn=True): """ Get Years for which CPI information is available for a given region. :param region: ISO Alpha 2 Code. :type region: ``str`` :param warn: warn if data could not be obtained. :type warn: ``bool`` :return: list of years for which CPI information is available. :rtype: ``list`` """ cpi_years_list = self._alpha2_cpi_record.get(region, []) if len(cpi_years_list): return cpi_years_list elif warn: raise KeyError("Could not obtain inflation (CPI) information for '%s' from the\n" \ "International Monetary Fund database currently cached." % (region)) else: return None def _cpi_match(self, region, year): """ Match region to the best possible year. :param region: region of the form allowed by `EasyPeasy().region_map()` :type region: ``str`` :param year: a year for which CPI information is desired. Can also be one of: 'oldest' or 'latest'. :type year: ``int`` or ``str`` :return: best matching of CPI information for the given region w.r.t. the year supplied. :rtype: ``float``, ``int`` or ``str`` """ # Initialize fall_back_year = None natural_region_name = None # replace year_b if it is 'oldest' or 'latest' available_years = list(map(int, self._cpi_years(region))) error_msg = "\nInflation (CPI) data for %s in '%s' could not be obtained from the\n" \ "International Monetary Fund database currently cached." warn_msg = error_msg + "\nFalling back to %s." if year == 'oldest': return min(available_years) elif year == 'latest': return max(available_years) elif int(float(year)) not in available_years: if self._fall_back: natural_region_name = self._pycountry_wrap.map_region_to_type(region, 'name') fall_back_year = closest_value(float(year), available_years) warn(warn_msg % (year, natural_region_name, str(fall_back_year))) return fall_back_year else: raise AttributeError(error_msg % (year, natural_region_name)) else: return year def _cpi_region_year(self, region, year): """ Get the Consumer Price Index (CPI) in a given region for a given year. :param region: region of the form allowed by `EasyPeasy().region_map()` :type region: ``str`` :param year: a year for which CPI information is desired. Can also be one of: 'oldest' or 'latest'. :type year: ``int`` or ``str`` :return: CPI for a given year. :rtype: ``float`` """ cpi = self._cpi_dict.get(str(int(float(year))), {}).get(self.region_map(region, 'alpha_2'), None) if cpi is not None: return float(cpi) else: raise KeyError("Could not obtain inflation information for '%s' in '%s'." % (str(region), str(year)))
[docs] def inflation(self, region, year_a, year_b=None, return_raw_cpi_dict=False, pretty_print=False): """ Calculator to compute the inflation rate from Consumer Price Index (CPI) information. Inflation Formula: .. math:: Inflation_{region} = \dfrac{c_{1} - c_{2}}{c_{2}} \cdot 100 | :math:`where`: | :math:`c_{1}` = CPI of the region in *year_b*. | :math:`c_{2}` = CPI of the region in *year_a*. :param region: a region. :type region: ``str`` :param year_a: start year. :type year_a: ``int`` :param year_b: end year. Defaults to None -- can only be left to this default if *return_raw_cpi_dict* is True. :type year_b: ``int`` :param return_raw_cpi_dict: If True, returns the CPI information in a dict. Defaults to False. :type return_raw_cpi_dict: ``bool`` :param pretty_print: if True, pretty prints the result otherwise returns the result as a float. Defaults to False. :type pretty_print: ``bool`` :return: (a) the rate of inflation between year_a and year_h. (b) a dictionary of CPI information with the years as keys, CPI as values. :rtype: ``float``, ``dict`` or ``NaN`` """ # Check Params self._params_check(pretty_print=pretty_print) # Map Region to its alpha 2 code mapped_region = self.region_map(region, 'alpha_2') # Set to_year to_year = self._cpi_match(mapped_region, year_b) if year_b is not None else None # Set from_year if year_a is not None: from_year = self._cpi_match(mapped_region, year_a) else: raise ValueError("year_a cannot be NoneType.") # Get the CPI for to_year and year_a c1 = self._cpi_region_year(mapped_region, to_year) if to_year is not None else None c2 = self._cpi_region_year(mapped_region, from_year) # Return dict, if requested if return_raw_cpi_dict != False: d = dict(zip(filter(None, [to_year, from_year]), filter(None, [c1, c2]))) if return_raw_cpi_dict == 'complete': return d, {"year_a": from_year, "year_b": to_year} elif return_raw_cpi_dict == True: return d # Compute the rate of Inflation (and round). if (any([pd.isnull(c1), pd.isnull(c2)])) or (c2 == 0.0): return np.NaN else: rate = round(((c1 - c2) / float(c2)) * 100, self._precision) # Return or Pretty Print. if not pretty_print: return rate else: print(rate, "%")
[docs] def inflation_calculator(self, amount, region, year_a, year_b, pretty_print=False): """ Adjusts a given amount of money for inflation. :param amount: a monetary amount. :type amount: ``float`` or ``int`` :param region: a geographical region. :type region: ``str`` :param year_a: start year. :type year_a: ``int`` :param year_b: end year. :type year_b: ``int`` :param pretty_print: if True, pretty prints the result otherwise returns the result as a float. Defaults to False. :type pretty_print: ``bool`` :return: :math:`amount \cdot inflation \space rate`. :rtype: ``float`` or ``NaN`` """ # Check Params self._params_check(amount, pretty_print) # Input checking if year_a == year_b: return mint(amount, self._precision, self.region_map(region, map_to='currency_alpha_3'), pretty_print) # Get the CPI information inflation_dict, years = self.inflation(region, year_a, year_b, return_raw_cpi_dict='complete') # Block division by zero if inflation_dict[years['year_a']] == 0: warn("Problem obtaining required inflation information.") return np.NaN # Scale w.r.t. inflation. adjusted_amount = inflation_dict[years['year_b']] / float(inflation_dict[years['year_a']]) * amount # Print or Return return mint(adjusted_amount, self._precision, self.region_map(region, map_to='currency_alpha_3'), pretty_print)
def _exchange_dates(self, currencies, min_max_rslt=False, date_format='%d/%m/%Y'): """ Get all dates for which there is data for a given list of currencies :param currencies: a list of currencies :type currencies: ``list`` :param min_max_rslt: compute the earliest and latest date for which exchange rate information is available. Defaults to False. :type min_max_rslt: ``bool`` :param date_format: a data format. Defaults to '%d/%m/%Y'. :type date_format: ``str`` :return: :rtype: 1D ``list`` or 2D ``list`` """ d = self._currency_date_record_range if min_max_rslt else self._currency_date_record dates = [d.get(c.upper(), None) for c in currencies] # Remove None dates = list(filter(None, dates)) if not len(dates): return None return dates[0] if len(dates) == 1 else dates def _user_currency_input(self, currency_or_region): """ Converts User supplied reference to a currency into an actual ISO Alpha 3 Currency Code. :param currency_or_region: reference to a currency :type currency_or_region: ``str`` :return: ISO Alpha 3 Currency Code :rtype: ``pycountry object`` """ # Note: 'temp. fix' has been added to handle currencies like 'EEK'. # This capability should be integrated into region_map() in the future. try: return pycountry.currencies.lookup(currency_or_region).alpha_3 except: if currency_or_region in self._ecb_currency_codes: # temp fix return currency_or_region else: return self.region_map(currency_or_region, "currency_alpha_3") def _base_cur_to_lcu(self, currency, date): """ Convert from a base currency (Euros) to a local currency unit, e.g., CAD. :param currency: a currency code or region :type currency: ``str`` :param date: date of allowed form (currently limited to DD/MM/YYYY). :type date: ``str`` :return: exchange_rate w.r.t. the Euro as a base currency. """ error_msg = "\nCould not obtain the exchange rate for '%s' on %s from the\n" \ "European Central Bank database currently cached." warn_msg = error_msg + "\nFalling back to %s." # Handle Base Currency if currency.upper() == 'EUR': return 1.0 available_data = self._currency_date_record.get(currency, None) if available_data == None: raise AttributeError("Data could not obtained for '%s' from the\n" \ "European Central Bank database currently cached." % (currency)) if date == 'oldest': exchange_date = self._currency_date_record_range.get(currency)[0] elif date == 'latest': exchange_date = self._currency_date_record_range.get(currency)[1] elif isinstance(date, str) and date not in available_data: if self._fall_back: exchange_date = closest_date(date, available_data) warn(warn_msg % (currency, date, exchange_date)) else: raise AttributeError(msg % (currency, exchange_date)) elif isinstance(date, str) and date_format_check(date, from_format="%d/%m/%Y"): exchange_date = date else: raise ValueError("Invalid Date Supplied. Dates must be of the form DD/MM/YYYY.") exchange_rate = self._exchange_dict.get(exchange_date, {}).get(currency, None) if not isinstance(exchange_rate, type(None)): return exchange_rate else: raise AttributeError(error_msg % (currency, exchange_date))
[docs] def currency_converter(self, amount, from_currency, to_currency, date="latest", pretty_print=False): """ Function to perform currency conversion based on, **not** directly reported from, data obtained from the European Central Bank (ECB). Base Currency: EUR. **Formulae Used** Let :math:`LCU` be defined as: .. math:: LCU(\phi_{EUR}, CUR) = ExchangeRate_{\phi_{EUR} → CUR} | :math:`where`: | :math:`CUR` is some local currency unit. | :math:`\phi = 1`, as in the ECB data. | That is, less formally: .. math:: LCU(\phi_{EUR}, CUR) = \dfrac{x \space \space CUR}{1 \space EUR} :math:`Thus`: From :math:`CUR_{1}` to :math:`CUR_{2}`: .. math:: amount_{CUR_{2}} = \dfrac{1}{LCU(\phi_{EUR}, CUR_{1})} \cdot LCU(\phi_{EUR}, CUR_{2}) \cdot amount_{CUR_{1}} :param amount: an amount of money to be converted. :type amount: ``float`` or ``int`` :param from_currency: the currency of the amount. :type from_currency: ``str`` :param to_currency: the currency the amount is to be converted into. :type to_currency: ``str`` :param date: date of data to perform the conversion with. Dates must be of the form: ``DD/MM/YYYY``. :type date: ``str`` :param pretty_print: if True, pretty prints the table otherwise returns the table as a pandas DataFrame. Defaults to False. :type pretty_print: ``bool`` :return: converted currency. :rtype: ``float`` """ # Check Params self._params_check(amount, pretty_print) # to/from_currency --> Currency Alpha 3 Code ca3 = [self._user_currency_input(c) if c not in self._ecb_currency_codes else c for c in (to_currency, from_currency)] to_currency_fn, from_currency_fn = ca3 # Return amount unaltered if self-conversion was requested. if from_currency_fn == to_currency_fn: return mint(amount, self._precision, to_currency_fn, pretty_print) if any(x == None or x is None for x in [to_currency_fn, from_currency_fn]): raise ValueError("Could not convert '%s' to '%s'." % (from_currency, to_currency)) # from_currency --> Base Currency --> to_currency conversion_to_invert = self._base_cur_to_lcu(from_currency_fn, date) if conversion_to_invert == 0.0: raise ZeroDivisionError("Cannot converted from '%s' on %s." % (from_currency, date)) converted_amount = (conversion_to_invert ** -1) * self._base_cur_to_lcu(to_currency_fn, date) * float(amount) # Return results (or pretty print) return mint(converted_amount, self._precision, to_currency_fn, pretty_print)
[docs] def normalize(self , amount , region , from_year , to_year="latest" , base_currency="EUR" , exchange_date="latest" , pretty_print=False): """ | Convert a Nominal Amount of money to a Real Amount in the same, or another, currency. | | This requires both inflation (for *currency*) and exchange rate information (*currency* to *base_currency*). See ``options(info = 'all', overlap_only = True)`` for an exhaustive listing of valid values to pass to this method. | | Currency Normalization occurs in two steps: | 1. Adjust the currency for inflation, e.g., 100 (2010 :math:`CUR_{1}`) → x (2015 :math:`CUR_{1}`). | 2. Convert the adjusted amount into the *base_currency*. :param amount: a numeric amount of money. :type amount: ``float`` or ``int`` :param currency: a region or currency. Legal options: Region Name, ISO Alpha2, Alpha3 or Currency Code (see ``options()``). :type currency: ``str`` :param from_year: a year. For valid values see ``options()``. :type from_year: ``int`` :param to_year: a year. For valid values see ``options()``. Defaults to 'latest' (which will use the most recent data available). :type to_year: ``str`` or ``int`` :param base_currency: a region or currency. Legal: Region Name, ISO Alpha2, Alpha3 or Currency Code (see ``options()``). Defaults to 'EUR'. :type base_currency: ``str`` :param pretty_print: Pretty print the result if True; return amount if False. Defaults to False. :type pretty_print: ``bool`` :return: amount adjusted for inflation and converted into the base currency. :rtype: ``float`` """ # Check Params self._params_check(amount, pretty_print) exchange_year = year_extract(exchange_date) if exchange_date not in ['oldest', 'latest'] and not str(exchange_year).isdigit(): warn("\n`exchange_date` is likely formatted improperly.") # Check delta between `to_year` and `exchange_date`. if abs(float(to_year) - float(exchange_year)) > 1: # note, this doesn't currently handle oldest v. latest. warn("\nThe year for which the input amount is being adjusted\n" "for inflation is %s, whereas the exchange rate year is %s." % (str(to_year), str(exchange_year))) # Adjust input for inflation real_amount = self.inflation_calculator(amount, region, year_a=from_year, year_b=to_year) # Compute Exchange normalize_amount = self.currency_converter(real_amount, region, base_currency, date=exchange_date) # Return results (or pretty print) return mint(normalize_amount, self._precision, self._user_currency_input(base_currency), pretty_print)
def _options_info_error(self, rformat): """ Error Messages for invalid information requests. :param rformat: one of: 'list' or 'table'. :type rformat: ``str`` """ options_error_msg = "Invalid Information Request.\n" \ "Please supply:\n" \ " - 'exchange' to `info` for a %s of currencies for which exchange rates are available.\n" \ " - 'inflation' to `info` for a %s of countries for which inflation information is available." \ % (rformat, rformat) if rformat == 'list': append = "\nTo see a complete summary, please set `rformat` equal to 'table' and set `info` equal to 'all'." elif rformat == 'table': append = "\n - 'all' to `info` for a complete summary of available data." raise ValueError(options_error_msg + append) def _options_table(self, info, table_overlap_only=False, range_table_dates=True): """ Tool to Generate Information for ``options()`` in table form. :param info: 'exchange', 'inflation' or 'all' ('all' requires rformat is set to 'table'). :type info: ``str`` :param table_overlap_only: :param table_overlap_only: when info is set to 'all', keep only those rows for which exchange rate and inflation data overlap. :type table_overlap_only: ``bool`` :param range_table_dates: if True, only report the minimum and maximum date for which data is available; if False, all dates for which data is available will be reported. Defaults to True. :type range_table_dates: ``bool`` :return: dataframe summarizing databases currently cached by ``EasyPeasy()``. :rtype: ``Pandas DataFrame`` """ # Note: does not currently handle currency transitions # Use CurrencyRelationshipsDB as Base d = [i for i in list(self._pycountry_wrap.alpha2_currency_dict.items()) if i[0] in self._pycountries_alpha_2] options_df = pd.DataFrame(d).rename(columns={0 : "Alpha2", 1 : "Currencies"}) # Add Names options_df['Region'] = [self._pycountry_wrap.map_region_to_type(c, 'name') for c in list(options_df['Alpha2'])] options_df['RegionFull'] = [self._pycountry_wrap.map_region_to_type(c, 'official_name') for c in list(options_df['Alpha2'])] # Add Alpha3 options_df['Alpha3'] = [self._pycountry_wrap.map_region_to_type(c, 'alpha_3') for c in list(options_df['Alpha2'])] # Map available Inflation Data onto this Base options_df['InflationDates'] = options_df['Alpha2'].map( lambda x: sort_range_reverse(self._cpi_years(x, warn=False), ('reverse' if not range_table_dates else 'range')), 'ignore' ) # Map available Exchange Rate Data onto this Base options_df['ExchangeDates'] = options_df['Currencies'].map( lambda x: self._exchange_dates(x, min_max_rslt=range_table_dates), na_action='ignore' ) # Add Overlap options_df['Overlap'] = options_df.apply( lambda x: year_date_overlap(x['InflationDates'], x['ExchangeDates'], date_format="%d/%m/%Y"), axis=1 ) # Fill NaNs options_df = options_df.fillna(np.NaN) # Weight Rows by Completeness; Sort by Data Overlap and Alpha2 Code options_df['TempRanking'] = options_df.apply(lambda x: options_ranking(x['InflationDates'], x['ExchangeDates']), axis=1) options_df = options_df.sort_values(['TempRanking', 'Region'], ascending=[False, True]) if table_overlap_only and info == 'all': options_df = pandas_null_drop(options_df, subset=['InflationDates', 'ExchangeDates']) elif table_overlap_only and info != 'all': warn("`table_overlap_only` can only take effect if `info` is equal to 'all'.") # Subset col_order = self._table_col_order if info == 'exchange': col_order = [c for c in self._table_col_order if c != 'InflationDates'] elif info == 'inflation': col_order = [c for c in self._table_col_order if c != 'ExchangeDates'] elif info != 'all': self._options_info_error('table') return options_df.drop('TempRanking', axis=1)[col_order].reset_index(drop=True) def _options_lists(self, info): """ Tool to Generate Information for ``options()`` in list form. :param info: 'exchange', 'inflation' or 'all' ('all' requires rformat is set to 'table'). :type info: ``str`` :return: list of requested information. :rtype: ``list`` """ if info.strip().lower() not in ['exchange', 'inflation']: self._options_info_error('list') d = self._exchange_dict if info.strip().lower() == 'exchange' else self._cpi_dict full = [list(v.keys()) for k, v in d.items()] return sorted(set([i for s in full for i in s]))
[docs] def options(self, info='all', rformat='table', pretty_print=True, table_overlap_only=False, range_table_dates=True): """ An easy interface to explore all of the terminology EasyPeasy understands as well the dates for which data is available. :param info: 'exchange', 'inflation' or 'all' ('all' requires rformat is set to 'table'). :type info: ``str`` :param rformat: 'table' for a table or 'list' for just the currency codes, alone. Defaults to 'table'. :type rformat: ``str`` :param pretty_print: if True, prints the list or table. If False, returns the list or table (as a Pandas DataFrame). Defaults to True. :type pretty_print: ``bool`` :param table_overlap_only: when info is set to 'all', keep only those rows for which exchange rate and inflation data overlap. :type table_overlap_only: ``bool`` :param range_table_dates: if True, only report the minimum and maximum date for which data is available; if False, all dates for which data is available will be reported. Defaults to True. :type range_table_dates: ``bool`` :return: information table or list :rtype: ``Pandas DataFrame`` or ``list`` """ pretty_df = None if rformat == 'list': request = self._options_lists(info) return request if not pretty_print else pprint(request, width=65, compact=True) elif rformat == 'table': request = self._options_table(info, table_overlap_only, range_table_dates) if pretty_print: pretty_df = request.drop('RegionFull', axis=1) pretty_df['Currencies'] = pretty_df['Currencies'].str.join("; ") pandas_pretty_print(pretty_df, col_align={'Region' : 'left'}) else: return request else: raise ValueError("`rformat` must be one of:\n" " - 'list', for a list of the requested information.\n" " - 'table', for a table (dataframe) of the requested information.")