readabs
Package to download timeseries data from the Australian Bureau of Statistics and RBA.
This package provides functions to download and process timeseries data from the Australian Bureau of Statistics (ABS) and the Reserve Bank of Australia (RBA).
1"""Package to download timeseries data from the Australian Bureau of Statistics and RBA. 2 3This package provides functions to download and process timeseries data from 4the Australian Bureau of Statistics (ABS) and the Reserve Bank of Australia (RBA). 5""" 6 7import importlib.metadata 8 9# ABS related imports 10from readabs.abs_catalogue import abs_catalogue 11from readabs.abs_meta_data import metacol 12 13# Utility imports 14from readabs.datatype import Datatype 15from readabs.grab_abs_url import grab_abs_url, grab_abs_zip 16from readabs.print_abs_catalogue import print_abs_catalogue 17 18# RBA related imports 19from readabs.rba_catalogue import print_rba_catalogue, rba_catalogue 20from readabs.rba_meta_data import rba_metacol 21from readabs.read_abs_by_desc import read_abs_by_desc 22from readabs.read_abs_cat import read_abs_cat 23from readabs.read_abs_series import read_abs_series 24from readabs.read_rba_table import read_rba_ocr, read_rba_table 25from readabs.read_support import ReadArgs 26from readabs.recalibrate import recalibrate, recalibrate_value 27from readabs.search_abs_meta import find_abs_id, search_abs_meta 28from readabs.splice import select, select_and_splice, select_one, splice 29from readabs.utilities import ( 30 annualise_percentages, 31 annualise_rates, 32 monthly_to_qtly, 33 percent_change, 34 qtly_to_monthly, 35) 36 37# Version and author information 38try: 39 __version__ = importlib.metadata.version(__name__) 40except importlib.metadata.PackageNotFoundError: 41 __version__ = "0.0.0" # Fallback for development mode 42__author__ = "Bryan Palmer" 43 44 45# Exposed functions and classes 46__all__ = ( 47 "Datatype", 48 "ReadArgs", 49 "abs_catalogue", 50 "annualise_percentages", 51 "annualise_rates", 52 "find_abs_id", 53 "grab_abs_url", 54 "grab_abs_zip", 55 "metacol", 56 "monthly_to_qtly", 57 "percent_change", 58 "print_abs_catalogue", 59 "print_rba_catalogue", 60 "qtly_to_monthly", 61 "rba_catalogue", 62 "rba_metacol", 63 "read_abs_by_desc", 64 "read_abs_cat", 65 "read_abs_series", 66 "read_rba_ocr", 67 "read_rba_table", 68 "recalibrate", 69 "recalibrate_value", 70 "search_abs_meta", 71 "select", 72 "select_and_splice", 73 "select_one", 74 "splice", 75) 76__pdoc__ = { 77 "download_cache": False, 78 "get_abs_links": False, 79 "read_support": False, 80 "grab_abs_url": False, 81} # hide submodules from documentation
39class ReadArgs(TypedDict): 40 """Type definition for ABS data reading arguments.""" 41 42 verbose: NotRequired[bool] 43 ignore_errors: NotRequired[bool] 44 get_zip: NotRequired[bool] 45 get_excel_if_no_zip: NotRequired[bool] 46 get_excel: NotRequired[bool] 47 single_zip_only: NotRequired[str] 48 single_excel_only: NotRequired[str] 49 selected_excel: NotRequired[tuple[str, ...]] 50 history: NotRequired[str] 51 cache_only: NotRequired[bool] 52 keep_non_ts: NotRequired[bool] 53 zip_file: NotRequired[str]
Type definition for ABS data reading arguments.
24@cache 25def abs_catalogue(*, cache_only: bool = False, verbose: bool = False) -> DataFrame: 26 """Return a DataFrame of ABS Catalogue numbers. 27 28 Downloads catalogue data from the ABS website on first call and caches 29 for future use. The returned DataFrame contains catalogue numbers with 30 their topics, themes, URLs, and status. 31 32 Parameters 33 ---------- 34 cache_only : bool, default False 35 If True, only use cached data and don't attempt to download. 36 verbose : bool, default False 37 If True, print progress messages. 38 39 Returns 40 ------- 41 DataFrame 42 DataFrame with columns ['Theme', 'Parent Topic', 'Topic', 'URL', 'Status'] 43 and index of catalogue IDs. 44 45 Raises 46 ------ 47 CatalogueError 48 If the catalogue data cannot be retrieved or parsed. 49 HttpError 50 If there's a network error downloading the catalogue. 51 CacheError 52 If cache_only=True but no cached data is available. 53 54 Example 55 ------- 56 >>> import readabs as ra 57 >>> catalogue = ra.abs_catalogue() 58 >>> print(catalogue.head()) 59 60 """ 61 try: 62 # Download ABS catalogue page 63 abs_bytes = get_file(ABS_CATALOGUE_URL, cache_only=cache_only, verbose=verbose) 64 65 if not abs_bytes: 66 raise CatalogueError("No data retrieved from ABS catalogue URL") 67 68 # Parse HTML content 69 try: 70 html_content = abs_bytes.decode(DEFAULT_ENCODING, errors="replace") 71 except UnicodeDecodeError as e: 72 raise CatalogueError(f"Failed to decode HTML content: {e}") from e 73 74 # Extract tables from HTML 75 try: 76 tables = read_html(StringIO(html_content), extract_links="body") 77 if not tables: 78 raise CatalogueError("No tables found in HTML content") 79 links = tables[-1] # Get the last table 80 except (ValueError, IndexError) as e: 81 raise CatalogueError(f"Failed to parse HTML tables: {e}") from e 82 83 # Validate required columns exist 84 required_cols = ["Catalogue number", "Topic"] 85 missing_cols = [col for col in required_cols if col not in links.columns] 86 if missing_cols: 87 raise CatalogueError(f"Missing required columns: {missing_cols}") 88 89 # Extract catalogue numbers and URLs 90 try: 91 cats = links["Catalogue number"].apply(Series)[0] 92 urls = links["Topic"].apply(Series)[1] 93 except (KeyError, IndexError) as e: 94 raise CatalogueError(f"Failed to extract catalogue data: {e}") from e 95 96 # Process topic URLs to create hierarchical structure 97 url_snippets = _process_topic_urls(urls) 98 99 # Create main DataFrame with hierarchical topic structure 100 frame = _create_topic_frame(url_snippets) 101 frame["URL"] = urls 102 103 # Align catalogue numbers with processed frame 104 cats = cats[frame.index] 105 106 # Process catalogue status (active vs ceased) 107 cat_index, status = _process_catalogue_status(cats) 108 109 frame["Status"] = status 110 frame.index = Index(cat_index) 111 frame.index.name = CATALOGUE_INDEX_NAME 112 113 except (HttpError, CacheError, ValueError) as e: 114 raise CatalogueError(f"Error retrieving ABS catalogue: {e}") from e 115 116 return frame
Return a DataFrame of ABS Catalogue numbers.
Downloads catalogue data from the ABS website on first call and caches for future use. The returned DataFrame contains catalogue numbers with their topics, themes, URLs, and status.
Parameters
cache_only : bool, default False If True, only use cached data and don't attempt to download. verbose : bool, default False If True, print progress messages.
Returns
DataFrame DataFrame with columns ['Theme', 'Parent Topic', 'Topic', 'URL', 'Status'] and index of catalogue IDs.
Raises
CatalogueError If the catalogue data cannot be retrieved or parsed. HttpError If there's a network error downloading the catalogue. CacheError If cache_only=True but no cached data is available.
Example
>>> import readabs as ra
>>> catalogue = ra.abs_catalogue()
>>> print(catalogue.head())
100def annualise_percentages(data: DataT, *, periods_per_year: float) -> DataT: 101 """Annualise a growth rate (expressed as a percentage) for a period. 102 103 Args: 104 data : pandas Series or DataFrame - The growth rate (expressed as a 105 percentage) to annualise. Note a growth percentage of 5% is a growth 106 rate of 0.05. 107 periods_per_year : int or float, default 12 - The number of periods in a 108 year. For monthly data, this is 12. 109 110 Returns: 111 pandas Series or DataFrame - The annualised growth expressed as a percentage. 112 For DataFrame input, the annualised growth rate is calculated for each column. 113 114 Raises: 115 InvalidParameterError - If periods_per_year is not positive. 116 InvalidDataError - If data is not a Series or DataFrame. 117 118 """ 119 if not isinstance(data, (Series, DataFrame)): 120 raise InvalidDataError("data must be a pandas Series or DataFrame") 121 122 if not isinstance(periods_per_year, (int, float)) or periods_per_year <= 0: 123 raise InvalidParameterError("periods_per_year must be a positive number") 124 125 try: 126 rates = data / 100.0 127 return annualise_rates(rates, periods_per_year=periods_per_year) 128 except Exception as e: 129 raise InvalidDataError(f"Error annualising percentages: {e}") from e
Annualise a growth rate (expressed as a percentage) for a period.
Args: data : pandas Series or DataFrame - The growth rate (expressed as a percentage) to annualise. Note a growth percentage of 5% is a growth rate of 0.05. periods_per_year : int or float, default 12 - The number of periods in a year. For monthly data, this is 12.
Returns: pandas Series or DataFrame - The annualised growth expressed as a percentage. For DataFrame input, the annualised growth rate is calculated for each column.
Raises: InvalidParameterError - If periods_per_year is not positive. InvalidDataError - If data is not a Series or DataFrame.
64def annualise_rates(data: DataT, *, periods_per_year: float) -> DataT: 65 """Annualise a growth rate for a period. 66 67 Note: returns a percentage value (and not a rate)! 68 69 Args: 70 data : pandas Series or DataFrame - The growth rate to annualise. 71 Note a growth rate of 0.05 is 5%. 72 periods_per_year : int or float, default 12 - The number of periods in a year. 73 For monthly data, this is 12. 74 75 Returns: 76 pandas Series or DataFrame - The annualised growth expressed as a percentage 77 (not a rate). For DataFrame input, the annualised growth rate is 78 calculated for each column. 79 80 Raises: 81 InvalidParameterError - If periods_per_year is not positive. 82 InvalidDataError - If data is not a Series or DataFrame. 83 84 """ 85 if not isinstance(data, (Series, DataFrame)): 86 raise InvalidDataError("data must be a pandas Series or DataFrame") 87 88 if not isinstance(periods_per_year, (int, float)) or periods_per_year <= 0: 89 raise InvalidParameterError("periods_per_year must be a positive number") 90 91 try: 92 # identical branches: see percent_change() 93 if isinstance(data, Series): 94 return (((1 + data) ** periods_per_year) - 1) * 100 95 return (((1 + data) ** periods_per_year) - 1) * 100 96 except Exception as e: 97 raise InvalidDataError(f"Error annualising rates: {e}") from e
Annualise a growth rate for a period.
Note: returns a percentage value (and not a rate)!
Args: data : pandas Series or DataFrame - The growth rate to annualise. Note a growth rate of 0.05 is 5%. periods_per_year : int or float, default 12 - The number of periods in a year. For monthly data, this is 12.
Returns: pandas Series or DataFrame - The annualised growth expressed as a percentage (not a rate). For DataFrame input, the annualised growth rate is calculated for each column.
Raises: InvalidParameterError - If periods_per_year is not positive. InvalidDataError - If data is not a Series or DataFrame.
131def find_abs_id( 132 meta: DataFrame, 133 search_terms: dict[str, str], 134 **kwargs: Unpack[SearchArgs], 135) -> tuple[str, str, str]: # table, series_id, units 136 """Find a unique ABS series identifier in the ABS metadata. 137 138 Parameters 139 ---------- 140 meta : DataFrame 141 A pandas DataFrame of metadata from the ABS 142 (via read_abs_cat() or read_abs_series()). 143 search_terms : dict[str, str] 144 A dictionary {search_phrase: meta_column_name, ...} of search terms. 145 Note: the search terms must be unique, as a dictionary cannot hold the 146 same search term to be applied to different columns. 147 **kwargs : Unpack[SearchArgs] 148 Keyword arguments passed on to search_abs_meta(): exact_match, 149 regex, validate_unique and verbose. Unknown keyword arguments 150 are reported, and not passed on. 151 validate_unique : bool = True 152 Raise a ValueError if the search result is not a single 153 unique match. Note: the default is True for safety. 154 155 Returns 156 ------- 157 tuple[str, str, str] 158 A tuple of the table, series_id and units for the unique 159 series_id that matches the search terms. 160 161 Metacol 162 ------- 163 Because the meta data is a DataFrame, the columns can be referenced by either 164 their full textual name, or by the short name defined in the metacol object. 165 For example, if metacol is imported as mc, to refer to the 166 `Data Item Description` column, the user can refer to it as mc.did. 167 168 Example 169 ------- 170 ```python 171 from readabs import metacol as mc # alias for the ABS meta data column names 172 from readabs import read_abs_cat, find_abs_id, recalibrate 173 cat_num = "6202.0" # The ABS labour force survey 174 data, meta = read_abs_cat(cat_num) 175 search_terms = { 176 "Employed total ; Persons ;": mc.did, 177 "Seasonally Adjusted": mc.stype, 178 "62020001": mc.table, 179 } 180 table, series_id, units = find_abs_id(meta, search_terms) 181 print(f"Table: {table} Series ID: {series_id} Units: {units}") 182 recal_series, recal_units = recalibrate(data[table][series_id], units) 183 ``` 184 185 """ 186 check_kwargs(kwargs, "find_abs_id", _SEARCH_KWARGS) # warn if invalid kwargs 187 188 # pass on only the known kwargs, so an unknown one is reported once 189 found = search_abs_meta( 190 meta, 191 search_terms, 192 exact_match=kwargs.get("exact_match", False), 193 regex=kwargs.get("regex", False), 194 validate_unique=kwargs.get("validate_unique", True), 195 verbose=kwargs.get("verbose", False), 196 ).iloc[0] 197 table, series_id, units = ( 198 found[mc.table], 199 found[mc.id], 200 found[mc.unit], 201 ) 202 203 return table, series_id, units
Find a unique ABS series identifier in the ABS metadata.
Parameters
meta : DataFrame A pandas DataFrame of metadata from the ABS (via read_abs_cat() or read_abs_series()). search_terms : dict[str, str] A dictionary {search_phrase: meta_column_name, ...} of search terms. Note: the search terms must be unique, as a dictionary cannot hold the same search term to be applied to different columns. **kwargs : Unpack[SearchArgs] Keyword arguments passed on to search_abs_meta(): exact_match, regex, validate_unique and verbose. Unknown keyword arguments are reported, and not passed on. validate_unique : bool = True Raise a ValueError if the search result is not a single unique match. Note: the default is True for safety.
Returns
tuple[str, str, str] A tuple of the table, series_id and units for the unique series_id that matches the search terms.
Metacol
Because the meta data is a DataFrame, the columns can be referenced by either
their full textual name, or by the short name defined in the metacol object.
For example, if metacol is imported as mc, to refer to the
Data Item Description column, the user can refer to it as mc.did.
Example
from readabs import metacol as mc # alias for the ABS meta data column names
from readabs import read_abs_cat, find_abs_id, recalibrate
cat_num = "6202.0" # The ABS labour force survey
data, meta = read_abs_cat(cat_num)
search_terms = {
"Employed total ; Persons ;": mc.did,
"Seasonally Adjusted": mc.stype,
"62020001": mc.table,
}
table, series_id, units = find_abs_id(meta, search_terms)
print(f"Table: {table} Series ID: {series_id} Units: {units}")
recal_series, recal_units = recalibrate(data[table][series_id], units)
37@cache # minimise slowness with repeat business 38def grab_abs_url( 39 cat: str = "", 40 url: str = "", 41 **kwargs: Unpack[ReadArgs], 42) -> dict[str, DataFrame]: 43 """For a given URL, extract the data from the Excel and ZIP file links found on that page. 44 45 The data is returned as a dictionary of DataFrames. The Excel files are converted 46 into DataFrames, with each sheet in each Excel file becoming a separate DataFrame. 47 ZIP files are examined for Excel files, which are similarly converted into 48 DataFrames. The dictionary of DataFrames is returned. 49 50 The preferred mechanism for reading ABS data is to use the `read_abs_cat()` 51 or `read_abs_series()` functions. This function is provided for those 52 cases where the data is not available in the ABS catalogue, where the 53 data is not a timeseries, or where the user wants to extract data from 54 a specific ABS landingpage. 55 56 57 Parameters 58 ---------- 59 url : str = "" 60 A URL for an ABS Catalogue landing page. Either a url or 61 a catalogue number must be provided. If both are provided, the 62 URL will be used. 63 64 cat : str = "" 65 An ABS Catalogue number. If provided, and the URL is not 66 provided, then the Catalogue number will be used to get the URL. 67 68 **kwargs : Unpack[ReadArgs] 69 Accepts the same keyword arguments as `read_abs_cat()`. 70 71 Returns 72 ------- 73 dict[str, DataFrame] 74 A dictionary of DataFrames. 75 76 """ 77 # check/get the keyword arguments 78 url = _get_url(url, cat) 79 check_kwargs(kwargs, "grab_abs_url") # warn if invalid kwargs 80 args = get_args(kwargs, "grab_abs_url") # get the valid kwargs 81 if verbose := args["verbose"]: 82 print(f"grab_abs_url(): {url=}, {args=}") 83 84 # get the URL links to the relevant ABS data files on that webpage 85 links = get_abs_links(url, **args) 86 if not links: 87 print(f"No data files found at URL: {url}") 88 return {} # return an empty Dictionary 89 90 # read the data files into a dictionary of DataFrames 91 abs_dict: dict[str, DataFrame] = {} 92 93 # Process single file requests first 94 abs_dict = _process_single_files(abs_dict, links, args, verbose=verbose) 95 if abs_dict: # If single file was found and processed, return it 96 return abs_dict 97 98 # Process all files based on configuration 99 return _process_all_files(abs_dict, links, args)
For a given URL, extract the data from the Excel and ZIP file links found on that page.
The data is returned as a dictionary of DataFrames. The Excel files are converted into DataFrames, with each sheet in each Excel file becoming a separate DataFrame. ZIP files are examined for Excel files, which are similarly converted into DataFrames. The dictionary of DataFrames is returned.
The preferred mechanism for reading ABS data is to use the read_abs_cat()
or read_abs_series() functions. This function is provided for those
cases where the data is not available in the ABS catalogue, where the
data is not a timeseries, or where the user wants to extract data from
a specific ABS landingpage.
Parameters
url : str = "" A URL for an ABS Catalogue landing page. Either a url or a catalogue number must be provided. If both are provided, the URL will be used.
cat : str = "" An ABS Catalogue number. If provided, and the URL is not provided, then the Catalogue number will be used to get the URL.
**kwargs : Unpack[ReadArgs]
Accepts the same keyword arguments as read_abs_cat().
Returns
dict[str, DataFrame] A dictionary of DataFrames.
102def grab_abs_zip( 103 zip_path: Path | str, 104 **kwargs: Unpack[ReadArgs] 105) -> dict[str, DataFrame]: 106 """Grab and process a single ABS ZIP file from a file system location. 107 108 This is a convenience function that opens an ABS ZIP file from a local 109 filesystem path. Expect to be used rarely. 110 111 Parameters 112 ---------- 113 zip_path : Path | str 114 The local filesystem path of the ABS ZIP file to open and process. 115 116 **kwargs : Unpack[ReadArgs] 117 Additional keyword arguments for file retrieval and processing. 118 119 Returns 120 ------- 121 dict[str, DataFrame] 122 A dictionary of DataFrames extracted from the ZIP file. 123 124 """ 125 check_kwargs(kwargs, "grab_abs_zip") # warn if invalid kwargs 126 args = get_args(kwargs, "grab_abs_zip") # get the valid kwargs 127 128 zp: Path = zip_path if isinstance(zip_path, Path) else Path(zip_path) 129 zip_bytes = zp.read_bytes() 130 abs_dict: dict[str, DataFrame] = {} 131 return _process_zip(abs_dict, zip_bytes, **args)
Grab and process a single ABS ZIP file from a file system location.
This is a convenience function that opens an ABS ZIP file from a local filesystem path. Expect to be used rarely.
Parameters
zip_path : Path | str The local filesystem path of the ABS ZIP file to open and process.
**kwargs : Unpack[ReadArgs] Additional keyword arguments for file retrieval and processing.
Returns
dict[str, DataFrame] A dictionary of DataFrames extracted from the ZIP file.
199def monthly_to_qtly(data: DataT, q_ending: str = "DEC", f: str = "mean") -> DataT: 200 """Convert monthly data to quarterly data. 201 202 This is done by taking the mean (or sum) of the three months in each quarter. 203 Ignore quarters with less than or more than three months data. Drop NA items. 204 Change f to "sum" for a quarterly sum. 205 206 Args: 207 data : pandas Series or DataFrame 208 The data to convert to quarterly frequency. 209 q_ending : str, default "DEC" 210 The month in which the quarter ends. For example, "DEC" for December. 211 f : str, default "mean" 212 The function to apply to the three months in each quarter. 213 Change to "sum" for a quarterly sum. The default is a 214 quarterly mean. 215 216 Returns: 217 pandas Series or DataFrame 218 The data with a quarterly PeriodIndex. If a quarter has less than 219 three months data, the quarter is dropped. If the quarter has more 220 than three months data, the quarter is dropped. Any NA data is removed. 221 For DataFrame input, the function is applied to each column. 222 223 Raises: 224 InvalidDataError - If data is not a Series or DataFrame, or does not 225 have a monthly PeriodIndex. 226 InvalidParameterError - If q_ending or f parameters are invalid. 227 228 """ 229 # Validate inputs 230 if not isinstance(data, (Series, DataFrame)): 231 raise InvalidDataError("data must be a pandas Series or DataFrame") 232 233 # non-monthly data would otherwise silently return an empty result 234 index = data.index 235 if not isinstance(index, PeriodIndex) or index.freqstr != "M": 236 raise InvalidDataError( 237 f"data must have a monthly PeriodIndex, got {type(index).__name__}" 238 f" with freq {getattr(index, 'freqstr', None)!r}" 239 ) 240 241 valid_endings = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] 242 if q_ending.upper() not in valid_endings: 243 raise InvalidParameterError(f"q_ending must be one of {valid_endings}") 244 245 valid_aggregations = ["mean", "sum", "min", "max", "std", "var"] 246 if f not in valid_aggregations: 247 raise InvalidParameterError(f"f must be one of {valid_aggregations}") 248 249 try: 250 if isinstance(data, Series): 251 return _monthly_to_qtly_series(data, q_ending, f) 252 # the guard above means this must be a DataFrame 253 return data.__class__({col: _monthly_to_qtly_series(data[col], q_ending, f) for col in data.columns}) 254 except Exception as e: 255 raise InvalidDataError(f"Error converting monthly to quarterly data: {e}") from e
Convert monthly data to quarterly data.
This is done by taking the mean (or sum) of the three months in each quarter. Ignore quarters with less than or more than three months data. Drop NA items. Change f to "sum" for a quarterly sum.
Args: data : pandas Series or DataFrame The data to convert to quarterly frequency. q_ending : str, default "DEC" The month in which the quarter ends. For example, "DEC" for December. f : str, default "mean" The function to apply to the three months in each quarter. Change to "sum" for a quarterly sum. The default is a quarterly mean.
Returns: pandas Series or DataFrame The data with a quarterly PeriodIndex. If a quarter has less than three months data, the quarter is dropped. If the quarter has more than three months data, the quarter is dropped. Any NA data is removed. For DataFrame input, the function is applied to each column.
Raises: InvalidDataError - If data is not a Series or DataFrame, or does not have a monthly PeriodIndex. InvalidParameterError - If q_ending or f parameters are invalid.
29def percent_change(data: DataT, n_periods: int) -> DataT: 30 """Calculate a percentage change in a contiguous, ordered series over n_periods. 31 32 Args: 33 data : pandas Series or DataFrame 34 The data to calculate the percentage change for. 35 n_periods : int 36 The number of periods to calculate the percentage change over. 37 Typically 4 for quarterly data, and 12 for monthly data. 38 39 Returns: 40 pandas Series or DataFrame - The percentage change in the data over n_periods. 41 For DataFrame input, the percentage change is calculated for each column. 42 43 Raises: 44 InvalidParameterError - If n_periods is not a positive integer. 45 InvalidDataError - If data is not a Series or DataFrame. 46 47 """ 48 if not isinstance(n_periods, int) or n_periods <= 0: 49 raise InvalidParameterError("n_periods must be a positive integer") 50 51 if not isinstance(data, (Series, DataFrame)): 52 raise InvalidDataError("data must be a pandas Series or DataFrame") 53 54 try: 55 # identical branches: pandas-stubs types Series[Any] scalar arithmetic as Any, 56 # which pyright cannot match to the Series constraint of DataT unless narrowed 57 if isinstance(data, Series): 58 return (data / data.shift(n_periods) - 1) * 100 59 return (data / data.shift(n_periods) - 1) * 100 60 except Exception as e: 61 raise InvalidDataError(f"Error calculating percentage change: {e}") from e
Calculate a percentage change in a contiguous, ordered series over n_periods.
Args: data : pandas Series or DataFrame The data to calculate the percentage change for. n_periods : int The number of periods to calculate the percentage change over. Typically 4 for quarterly data, and 12 for monthly data.
Returns: pandas Series or DataFrame - The percentage change in the data over n_periods. For DataFrame input, the percentage change is calculated for each column.
Raises: InvalidParameterError - If n_periods is not a positive integer. InvalidDataError - If data is not a Series or DataFrame.
11def print_abs_catalogue(*, cache_only: bool = False, verbose: bool = False) -> None: 12 """Print a table of ABS Catalogue Numbers with their metadata. 13 14 Displays catalogue numbers that contain time-series data along with 15 their theme, parent topic, topic, and status information. The URL 16 column is excluded from the display for readability. 17 18 This is primarily a convenience function to help users identify the 19 correct catalogue number for data retrieval functions. 20 21 Parameters 22 ---------- 23 cache_only : bool, default False 24 If True, only use cached catalogue data. 25 verbose : bool, default False 26 If True, print progress messages during catalogue retrieval. 27 28 Raises 29 ------ 30 CatalogueError 31 If the catalogue data cannot be retrieved or processed. 32 HttpError 33 If there's a network error downloading the catalogue. 34 CacheError 35 If cache_only=True but no cached data is available. 36 37 Example 38 ------- 39 >>> import readabs as ra 40 >>> ra.print_abs_catalogue() 41 42 """ 43 try: 44 # Retrieve the catalogue data 45 catalogue = abs_catalogue(cache_only=cache_only, verbose=verbose) 46 47 # Validate catalogue is not empty 48 if catalogue.empty: 49 print("No catalogue data available.") 50 return 51 52 # Select columns for display (exclude URL for readability) 53 available_columns = [col for col in DISPLAY_COLUMNS if col in catalogue.columns] 54 if not available_columns: 55 print("Catalogue data does not contain expected columns.") 56 return 57 58 display_data = catalogue[available_columns] 59 60 # Generate and print markdown table 61 try: 62 markdown_output = display_data.to_markdown() 63 print(markdown_output) 64 except Exception: # noqa: BLE001 65 print(display_data.to_string()) 66 67 except (CatalogueError, HttpError, CacheError) as e: 68 print(f"Error retrieving catalogue: {e}")
Print a table of ABS Catalogue Numbers with their metadata.
Displays catalogue numbers that contain time-series data along with their theme, parent topic, topic, and status information. The URL column is excluded from the display for readability.
This is primarily a convenience function to help users identify the correct catalogue number for data retrieval functions.
Parameters
cache_only : bool, default False If True, only use cached catalogue data. verbose : bool, default False If True, print progress messages during catalogue retrieval.
Raises
CatalogueError If the catalogue data cannot be retrieved or processed. HttpError If there's a network error downloading the catalogue. CacheError If cache_only=True but no cached data is available.
Example
>>> import readabs as ra
>>> ra.print_abs_catalogue()
47def print_rba_catalogue(*, cache_only: bool = False, verbose: bool = False) -> None: 48 """Print to standard output a table of the RBA Catalogue Numbers. 49 50 This function prints a formatted table of RBA catalogue numbers 51 to standard output. 52 53 Parameters 54 ---------- 55 cache_only : bool = False 56 If True, only use the cache. 57 verbose : bool = False 58 If True, print progress messages. 59 60 Returns 61 ------- 62 None 63 This function does not return anything. 64 65 Example 66 ------- 67 68 ```python 69 import readabs as ra 70 ra.print_rba_catalogue() 71 ``` 72 73 """ 74 rba_catalog = rba_catalogue(cache_only=cache_only, verbose=verbose) 75 print(rba_catalog.loc[:, rba_catalog.columns != "URL"].to_markdown())
Print to standard output a table of the RBA Catalogue Numbers.
This function prints a formatted table of RBA catalogue numbers to standard output.
Parameters
cache_only : bool = False If True, only use the cache. verbose : bool = False If True, print progress messages.
Returns
None This function does not return anything.
Example
import readabs as ra
ra.print_rba_catalogue()
132def qtly_to_monthly( 133 data: DataT, 134 *, 135 interpolate: bool = True, 136 limit: int | None = 2, # only used if interpolate is True 137 dropna: bool = True, 138) -> DataT: 139 """Convert data from Quarterly PeriodIndex to a Monthly PeriodIndex. 140 141 Args: 142 data: Series or DataFrame with quarterly PeriodIndex. Assumes the index is unique. 143 The data to convert to monthly frequency. 144 interpolate: bool, default True 145 Whether to interpolate the missing monthly data. 146 limit: int, default 2 - The maximum number of consecutive missing months 147 to interpolate. 148 dropna: bool, default True - Whether to drop NA data 149 150 Returns: 151 pandas Series or DataFrame - The data with a Monthly PeriodIndex. 152 If interpolate is True, the missing monthly data is interpolated. 153 If dropna is True, any NA data is removed. 154 155 Raises: 156 InvalidDataError - If data index is not a quarterly PeriodIndex or has issues. 157 InvalidParameterError - If limit parameter is invalid. 158 159 """ 160 # Validate input data 161 if not isinstance(data, (Series, DataFrame)): 162 raise InvalidDataError("data must be a pandas Series or DataFrame") 163 164 if not isinstance(data.index, PeriodIndex): 165 raise InvalidDataError("data index must be a PeriodIndex") 166 167 if not (data.index.freqstr and data.index.freqstr[0] == "Q"): 168 raise InvalidDataError("data index must have quarterly frequency") 169 170 if not data.index.is_unique: 171 raise InvalidDataError("data index must be unique") 172 173 if not data.index.is_monotonic_increasing: 174 raise InvalidDataError("data index must be monotonic increasing") 175 176 if limit is not None and (not isinstance(limit, int) or limit < 0): 177 raise InvalidParameterError("limit must be a non-negative integer or None") 178 179 # do the heavy lifting 180 try: 181 data = ( 182 data.set_axis(labels=data.index.to_timestamp(how="end"), axis="index") 183 .resample(rule="ME") # adds in every missing month 184 .first(min_count=1) # generates nans for new months 185 # assumes only one value per quarter (ie. unique index) 186 .pipe(_set_axis_monthly_periods) 187 ) 188 except Exception as e: 189 raise InvalidDataError(f"Error in quarterly to monthly conversion: {e}") from e 190 191 if interpolate: 192 data = data.interpolate(limit_area="inside", limit=limit) 193 if dropna: 194 data = data.dropna() 195 196 return data
Convert data from Quarterly PeriodIndex to a Monthly PeriodIndex.
Args: data: Series or DataFrame with quarterly PeriodIndex. Assumes the index is unique. The data to convert to monthly frequency. interpolate: bool, default True Whether to interpolate the missing monthly data. limit: int, default 2 - The maximum number of consecutive missing months to interpolate. dropna: bool, default True - Whether to drop NA data
Returns: pandas Series or DataFrame - The data with a Monthly PeriodIndex. If interpolate is True, the missing monthly data is interpolated. If dropna is True, any NA data is removed.
Raises: InvalidDataError - If data index is not a quarterly PeriodIndex or has issues. InvalidParameterError - If limit parameter is invalid.
17@cache 18def rba_catalogue(*, cache_only: bool = False, verbose: bool = False) -> DataFrame: 19 """Return a DataFrame of RBA Catalogue numbers. 20 21 In the first instance, this is downloaded from the RBA website, and 22 cached for future use. 23 24 Parameters 25 ---------- 26 cache_only : bool = False 27 If True, only use the cache. 28 verbose : bool = False 29 If True, print progress messages. 30 31 Returns 32 ------- 33 DataFrame 34 A DataFrame of RBA Catalogue numbers. 35 36 Example 37 ------- 38 ```python 39 import readabs as ra 40 catalogue = ra.rba_catalogue() 41 ``` 42 43 """ 44 return _get_rba_links(cache_only=cache_only, verbose=verbose)
Return a DataFrame of RBA Catalogue numbers.
In the first instance, this is downloaded from the RBA website, and cached for future use.
Parameters
cache_only : bool = False If True, only use the cache. verbose : bool = False If True, print progress messages.
Returns
DataFrame A DataFrame of RBA Catalogue numbers.
Example
import readabs as ra
catalogue = ra.rba_catalogue()
144def read_abs_by_desc( 145 wanted: list[str] | dict[str, str] | dict[str, dict[str, Any]], 146 **kwargs: Any, 147) -> tuple[dict[str, pd.Series], pd.DataFrame]: 148 """Get specific ABS data series by searching the ABS meta data. 149 150 Parameters 151 ---------- 152 wanted : list of str, dict of str:str, or dict of str:dict 153 The data 154 item descriptions to search for. If a list, it will be a list of 155 descriptions to search for. If a dictionary, the keys will a name. 156 The dictionary values can be either a string (the data item 157 description to search for) or a dictionary of keyword arguments, one of 158 which would be the data item description to search for. 159 **kwargs : Any 160 Keyword arguments to control the data retrieval. 161 The keyword arguments can include the following: 162 - abs_dict : dict - the dictionary of ABS data to search (from 163 read_abs_cat()). 164 - abs_meta : DataFrame - the metadata for the ABS data (from 165 read_abs_cat()). 166 - for the retrieval of data, the "cat" argument must be present. 167 The following arguments, if present, will also be used (ie. 168 passed to read_abs_cat()): ["ignore_errors", "get_zip", 169 "get_excel_if_no_zip", "get_excel", "cache_only", 170 "single_excel_only", "selected_excel", "single_zip_only", 171 "verbose"]. 172 - for the selection of data, the following metacol names, if present, 173 will be used to construct the selector: "cat", "did" 174 "stype", "id", "start", "end", "num", "unit", "dtype", "freq", 175 "cmonth", "table", "tdesc". 176 - finally, the following arguments will be passed to the find_abs_id() 177 and search_abs_meta() functions: ["validate_unique", "exact_match", 178 "regex", "verbose"]. 179 180 Notes 181 ----- 182 - if "wanted" is of type list[str] or dict[str, str], the kwargs should 183 include sufficient keys from the metacol dataclass to get the data. 184 Typically, the "cat" key, the "table" key, and the "stype" key would 185 be required. The did key would taken from the wanted list or 186 dictionary. 187 if wanted is of type dict[str, dict[str, Any]], the inner dictionary 188 must contain a "did" key. The other keys that can be used for the 189 data retrieval are the same as the metacol dataclass fileds, namely: 190 "cat", "stype", "id", "start", "end", "num", "unit", "dtype", "freq", 191 "cmonth", "table", "tdesc". 192 - if abs_dict and abs_meta are provided within the kwargs, they will be 193 used to locate and extract the selected data. 194 - if abs_dict and abs_meta are not provided, then, (1) wanted must be of 195 type dict[str, dict[str, Any]] and (2) the inner dictionary must 196 contain a "cat" key so the data can be retrieved. Other keys that 197 can be used for the data retrieval are the same as for read_abs_cat(), 198 namely ["ignore_errors", "get_zip", "get_excel_if_no_zip", 199 "get_excel", "single_excel_only", "selected_excel", 200 "single_zip_only", "cache_only"]. 201 202 203 Returns 204 ------- 205 Returns a tuple of two items: 206 - A dictionary of pandas Series objects, where the keys are the series 207 descriptions. The series.name attribute will be the ABS series-id. 208 - A pandas DataFrame containing the metadata for the series. 209 210 Example 211 ------- 212 213 ```python 214 import readabs as ra 215 from pandas import DataFrame 216 cat_num = "5206.0" # The ABS National Accounts 217 data, meta = ra.read_abs_cat(cat=cat_num) 218 wanted = ["Gross domestic product: Chain volume measures ;",] 219 selected, selected_meta = ra.read_abs_by_desc( 220 wanted=wanted, abs_dict=data, abs_meta=meta, table="5206001_Key_Aggregates" 221 ) 222 ``` 223 224 """ 225 # - preparation 226 if not _work_to_do(wanted): 227 return {}, pd.DataFrame() 228 if isinstance(wanted, list): 229 wanted = _wlist_to_wdict(wanted) 230 abs_dict = kwargs.get("abs_dict", {}) 231 abs_meta = kwargs.get("abs_meta", pd.DataFrame()) 232 kwarg_selector = _get_search_terms(kwargs, {}) 233 search_args = _get_search_args(kwargs, {}) 234 235 return_dict = {} 236 return_meta = pd.DataFrame() 237 for key, value in wanted.items(): 238 item_selector = kwarg_selector.copy() 239 item_search_args = search_args.copy() 240 if isinstance(value, str): 241 series, meta = _get_item_from_str( 242 item=value, 243 data_dict=abs_dict, 244 data_meta=abs_meta, 245 item_selector=item_selector, 246 search_args=item_search_args, 247 ) 248 249 elif isinstance(value, dict): 250 series, meta = _get_item_from_dict( 251 item_dict=value, 252 data_dict=abs_dict, 253 data_meta=abs_meta, 254 item_selector=item_selector, 255 search_args=item_search_args, 256 **kwargs, 257 ) 258 else: 259 raise TypeError( 260 "Each value in the wanted list/dictionary must be either a string " + "or a dictionary." 261 ) 262 263 # save search results 264 return_dict[key] = series 265 return_meta = pd.concat([return_meta, meta]) 266 267 return return_dict, return_meta
Get specific ABS data series by searching the ABS meta data.
Parameters
wanted : list of str, dict of str:str, or dict of str:dict The data item descriptions to search for. If a list, it will be a list of descriptions to search for. If a dictionary, the keys will a name. The dictionary values can be either a string (the data item description to search for) or a dictionary of keyword arguments, one of which would be the data item description to search for. **kwargs : Any Keyword arguments to control the data retrieval. The keyword arguments can include the following: - abs_dict : dict - the dictionary of ABS data to search (from read_abs_cat()). - abs_meta : DataFrame - the metadata for the ABS data (from read_abs_cat()). - for the retrieval of data, the "cat" argument must be present. The following arguments, if present, will also be used (ie. passed to read_abs_cat()): ["ignore_errors", "get_zip", "get_excel_if_no_zip", "get_excel", "cache_only", "single_excel_only", "selected_excel", "single_zip_only", "verbose"]. - for the selection of data, the following metacol names, if present, will be used to construct the selector: "cat", "did" "stype", "id", "start", "end", "num", "unit", "dtype", "freq", "cmonth", "table", "tdesc". - finally, the following arguments will be passed to the find_abs_id() and search_abs_meta() functions: ["validate_unique", "exact_match", "regex", "verbose"].
Notes
- if "wanted" is of type list[str] or dict[str, str], the kwargs should include sufficient keys from the metacol dataclass to get the data. Typically, the "cat" key, the "table" key, and the "stype" key would be required. The did key would taken from the wanted list or dictionary. if wanted is of type dict[str, dict[str, Any]], the inner dictionary must contain a "did" key. The other keys that can be used for the data retrieval are the same as the metacol dataclass fileds, namely: "cat", "stype", "id", "start", "end", "num", "unit", "dtype", "freq", "cmonth", "table", "tdesc".
- if abs_dict and abs_meta are provided within the kwargs, they will be used to locate and extract the selected data.
- if abs_dict and abs_meta are not provided, then, (1) wanted must be of type dict[str, dict[str, Any]] and (2) the inner dictionary must contain a "cat" key so the data can be retrieved. Other keys that can be used for the data retrieval are the same as for read_abs_cat(), namely ["ignore_errors", "get_zip", "get_excel_if_no_zip", "get_excel", "single_excel_only", "selected_excel", "single_zip_only", "cache_only"].
Returns
Returns a tuple of two items:
- A dictionary of pandas Series objects, where the keys are the series descriptions. The series.name attribute will be the ABS series-id.
- A pandas DataFrame containing the metadata for the series.
Example
import readabs as ra
from pandas import DataFrame
cat_num = "5206.0" # The ABS National Accounts
data, meta = ra.read_abs_cat(cat=cat_num)
wanted = ["Gross domestic product: Chain volume measures ;",]
selected, selected_meta = ra.read_abs_by_desc(
wanted=wanted, abs_dict=data, abs_meta=meta, table="5206001_Key_Aggregates"
)
28@cache # minimise slowness for any repeat business 29def read_abs_cat( 30 cat: str, 31 url: str = "", 32 **kwargs: Unpack[ReadArgs], 33) -> tuple[dict[str, DataFrame], DataFrame]: 34 """For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames. 35 36 This function returns the complete ABS Catalogue information as a 37 python dictionary of pandas DataFrames, as well as the associated metadata 38 in a separate DataFrame. The function automates the collection of zip and 39 excel files from the ABS website. If necessary, these files are downloaded, 40 and saved into a cache directory. The files are then parsed to extract time 41 series data, and the associated metadata. 42 43 By default, the cache directory is `./.readabs_cache/`. You can change the 44 default directory name by setting the shell environment variable 45 `READABS_CACHE_DIR` with the name of the preferred directory. 46 47 Parameters 48 ---------- 49 cat : str 50 The ABS Catalogue Number for the data to be downloaded and made 51 available by this function. This argument must be specified in the 52 function call. 53 54 **kwargs : Unpack[ReadArgs] 55 The following parameters may be passed as optional keyword arguments. 56 57 url : str = "" 58 The URL of an ABS landing page. Use this for discontinued series 59 that are no longer in the ABS Time Series Directory. If provided, 60 data will be retrieved from this URL instead of looking up the 61 catalogue number. Example: 62 `read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")` 63 64 keep_non_ts : bool = False 65 A flag for whether to keep the non-time-series tables 66 that might form part of an ABS catalogue item. Normally, the 67 non-time-series information is ignored, and not made available to 68 the user. 69 70 history : str = "" 71 Provide a month-year string to extract historical ABS data. 72 For example, you can set history="dec-2023" to the get the ABS data 73 for a catalogue identifier that was originally published in respect 74 of Q4 of 2023. Note: not all ABS data sources are structured so that 75 this technique works in every case; but most are. 76 77 verbose : bool = False 78 Setting this to true may help diagnose why something 79 might be going wrong with the data retrieval process. 80 81 ignore_errors : bool = False 82 Normally, this function will cease downloading when 83 an error in encountered. However, sometimes the ABS website has 84 malformed links, and changing this setting is necessitated. (Note: 85 if you drop a message to the ABS, they will usually fix broken 86 links with a business day). 87 88 get_zip : bool = True 89 Download the excel files in .zip files. 90 91 get_excel_if_no_zip : bool = True 92 Only try to download .xlsx files if there are no zip 93 files available to be downloaded. Only downloading individual excel 94 files when there are no zip files to download can speed up the 95 download process. 96 97 get_excel : bool = False 98 The default value means that excel files are not 99 automatically download. Note: at least one of `get_zip`, 100 `get_excel_if_no_zip`, or `get_excel` must be true. For most ABS 101 catalogue items, it is sufficient to just download the one zip 102 file. But note, some catalogue items do not have a zip file. 103 Others have quite a number of zip files. 104 105 single_excel_only : str = "" 106 If this argument is set to a table name (without the 107 .xlsx extension), only that excel file will be downloaded. If 108 set, and only a limited subset of available data is needed, 109 this can speed up download times significantly. Note: overrides 110 `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`. 111 112 selected_excel : tuple[str, ...] = () 113 If set to a tuple of table names (without the .xlsx extension), 114 only those excel files will be downloaded. Useful when several 115 specific tables are needed and downloading the full zip would 116 be wasteful. Example: 117 `selected_excel=("62020001", "62020017", "62020X28")`. 118 Must be a tuple (not a list) because `read_abs_cat` uses an 119 internal cache that requires hashable arguments. Note: overrides 120 `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only` 121 when at least one matching file is found. 122 123 single_zip_only : str = "" 124 If this argument is set to a zip file name (without 125 the .zip extension), only that zip file will be downloaded. 126 If set, and only a limited subset of available data is needed, 127 this can speed up download times significantly. Note: overrides 128 `get_zip`, `get_excel_if_no_zip`, and `get_excel`. 129 130 cache_only : bool = False 131 If set to True, this function will only access 132 data that has been previously cached. Normally, the function 133 checks the date of the cache data against the date of the data 134 on the ABS website, before deciding whether the ABS has fresher 135 data that needs to be downloaded to the cache. 136 137 zip_file: str | Path = "" 138 If set to a specific zip file name (with or without the .zip 139 extension), this function will only extract data from that zip file 140 on the local file system. This may be useful for debugging purposes. 141 142 Returns 143 ------- 144 tuple[dict[str, DataFrame], DataFrame] 145 The function returns a tuple of two items. The first item is a 146 python dictionary of pandas DataFrames (which is the primary data 147 associated with the ABS catalogue item). The second item is a 148 DataFrame of ABS metadata for the ABS collection. 149 150 Note: 151 You can retrieve non-timeseries data using the grab_abs_url() 152 function. That takes the URL for the ABS landing page for the ABS 153 collection you are interested in. The read_abs_cat function is for 154 ABS catalogue identifiers which are timeseries data, for which the 155 metadata can be extracted. 156 157 Example 158 ------- 159 160 ```python 161 import readabs as ra 162 from pandas import DataFrame 163 cat_num = "6202.0" # The ABS labour force survey 164 data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num) 165 abs_dict, meta = data 166 ``` 167 168 """ 169 # --- get the time series data --- 170 zip_file = kwargs.get("zip_file") 171 raw_abs_dict = ( 172 grab_abs_zip(zip_file, **kwargs) if zip_file else grab_abs_url(cat=cat, url=url, **kwargs) 173 ) 174 response = _get_time_series_data(cat, raw_abs_dict, **kwargs) 175 176 if not response: 177 response = {}, DataFrame() 178 179 return response # dictionary of DataFrames, and a DataFrame of metadata
For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames.
This function returns the complete ABS Catalogue information as a python dictionary of pandas DataFrames, as well as the associated metadata in a separate DataFrame. The function automates the collection of zip and excel files from the ABS website. If necessary, these files are downloaded, and saved into a cache directory. The files are then parsed to extract time series data, and the associated metadata.
By default, the cache directory is ./.readabs_cache/. You can change the
default directory name by setting the shell environment variable
READABS_CACHE_DIR with the name of the preferred directory.
Parameters
cat : str The ABS Catalogue Number for the data to be downloaded and made available by this function. This argument must be specified in the function call.
**kwargs : Unpack[ReadArgs] The following parameters may be passed as optional keyword arguments.
url : str = ""
The URL of an ABS landing page. Use this for discontinued series
that are no longer in the ABS Time Series Directory. If provided,
data will be retrieved from this URL instead of looking up the
catalogue number. Example:
read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")
keep_non_ts : bool = False A flag for whether to keep the non-time-series tables that might form part of an ABS catalogue item. Normally, the non-time-series information is ignored, and not made available to the user.
history : str = "" Provide a month-year string to extract historical ABS data. For example, you can set history="dec-2023" to the get the ABS data for a catalogue identifier that was originally published in respect of Q4 of 2023. Note: not all ABS data sources are structured so that this technique works in every case; but most are.
verbose : bool = False Setting this to true may help diagnose why something might be going wrong with the data retrieval process.
ignore_errors : bool = False Normally, this function will cease downloading when an error in encountered. However, sometimes the ABS website has malformed links, and changing this setting is necessitated. (Note: if you drop a message to the ABS, they will usually fix broken links with a business day).
get_zip : bool = True Download the excel files in .zip files.
get_excel_if_no_zip : bool = True Only try to download .xlsx files if there are no zip files available to be downloaded. Only downloading individual excel files when there are no zip files to download can speed up the download process.
get_excel : bool = False
The default value means that excel files are not
automatically download. Note: at least one of get_zip,
get_excel_if_no_zip, or get_excel must be true. For most ABS
catalogue items, it is sufficient to just download the one zip
file. But note, some catalogue items do not have a zip file.
Others have quite a number of zip files.
single_excel_only : str = ""
If this argument is set to a table name (without the
.xlsx extension), only that excel file will be downloaded. If
set, and only a limited subset of available data is needed,
this can speed up download times significantly. Note: overrides
get_zip, get_excel_if_no_zip, get_excel and single_zip_only.
selected_excel : tuple[str, ...] = ()
If set to a tuple of table names (without the .xlsx extension),
only those excel files will be downloaded. Useful when several
specific tables are needed and downloading the full zip would
be wasteful. Example:
selected_excel=("62020001", "62020017", "62020X28").
Must be a tuple (not a list) because read_abs_cat uses an
internal cache that requires hashable arguments. Note: overrides
get_zip, get_excel_if_no_zip, get_excel and single_zip_only
when at least one matching file is found.
single_zip_only : str = ""
If this argument is set to a zip file name (without
the .zip extension), only that zip file will be downloaded.
If set, and only a limited subset of available data is needed,
this can speed up download times significantly. Note: overrides
get_zip, get_excel_if_no_zip, and get_excel.
cache_only : bool = False If set to True, this function will only access data that has been previously cached. Normally, the function checks the date of the cache data against the date of the data on the ABS website, before deciding whether the ABS has fresher data that needs to be downloaded to the cache.
zip_file: str | Path = "" If set to a specific zip file name (with or without the .zip extension), this function will only extract data from that zip file on the local file system. This may be useful for debugging purposes.
Returns
tuple[dict[str, DataFrame], DataFrame] The function returns a tuple of two items. The first item is a python dictionary of pandas DataFrames (which is the primary data associated with the ABS catalogue item). The second item is a DataFrame of ABS metadata for the ABS collection.
Note:
You can retrieve non-timeseries data using the grab_abs_url()
function. That takes the URL for the ABS landing page for the ABS
collection you are interested in. The read_abs_cat function is for
ABS catalogue identifiers which are timeseries data, for which the
metadata can be extracted.
Example
import readabs as ra
from pandas import DataFrame
cat_num = "6202.0" # The ABS labour force survey
data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num)
abs_dict, meta = data
15def read_abs_series( 16 cat: str, 17 series_id: str | Sequence[str], 18 url: str = "", 19 **kwargs: Unpack[ReadArgs], 20) -> tuple[DataFrame, DataFrame]: 21 """Get specific ABS data series by their ABS catalogue and series identifiers. 22 23 Parameters 24 ---------- 25 cat : str 26 The ABS catalogue ID. 27 28 series_id : str | Sequence[str] 29 An ABS series ID or a sequence of ABS series IDs. 30 31 url : str = "" 32 The URL of an ABS landing page. Use this for discontinued series 33 that are no longer in the ABS Time Series Directory. If provided, 34 data is retrieved from this URL instead of looking up the catalogue 35 number. Passed through to read_abs_cat(). 36 37 **kwargs : Any 38 Keyword arguments for the read_abs_series function, 39 which are the same as the keyword arguments for the 40 read_abs_cat function. 41 42 Returns 43 ------- 44 tuple[DataFrame, DataFrame] 45 A tuple of two DataFrames, one for the primary data and one for the metadata. 46 47 Example 48 ------- 49 50 ```python 51 import readabs as ra 52 from pandas import DataFrame 53 cat_num = "6202.0" # The ABS labour force survey 54 unemployment_rate = "A84423050A" 55 seo = "62020001" # The ABS table name 56 data, meta = ra.read_abs_series( 57 cat=cat_num, series_id=unemployment_rate, single_excel_only=seo 58 ) 59 ``` 60 61 """ 62 # check for unexpected keyword arguments/get defaults 63 check_kwargs(kwargs, "read_abs_series") 64 args = get_args(kwargs, "read_abs_series") 65 66 # read the ABS category data 67 cat_data, cat_meta = read_abs_cat(cat, url=url, **args) 68 69 # drop repeated series_ids in the meta data, 70 # make unique series_ids the index 71 cat_meta.index = Index(cat_meta[metacol.id]) 72 cat_meta = cat_meta.groupby(cat_meta.index).first() 73 74 # get the ABS series data 75 if isinstance(series_id, str): 76 series_id = [series_id] 77 return_data, return_meta = DataFrame(), DataFrame() 78 for identifier in series_id: 79 # confirm that the series ID is in the catalogue 80 if identifier not in cat_meta.index: 81 if args["verbose"]: 82 print(f"Series ID {identifier} not found in ABS catalogue ID {cat}") 83 if args["ignore_errors"]: 84 continue 85 raise ValueError(f"Series ID {identifier} not found in catalogue {cat}") 86 87 # confirm thay the index of the series is compatible 88 table = str(cat_meta.loc[identifier, metacol.table]) # str for mypy 89 data_series = cat_data[table][identifier] 90 if ( 91 len(return_data) > 0 92 and cast("PeriodIndex", return_data.index).freq != cast("PeriodIndex", data_series.index).freq 93 ): 94 if args["verbose"]: 95 print(f"Frequency mismatch for series ID {identifier}") 96 if args["ignore_errors"]: 97 continue 98 raise ValueError(f"Frequency mismatch for series ID {identifier}") 99 100 # add the series data and meta data to the return values 101 if len(return_data) > 0: 102 return_data = return_data.reindex(return_data.index.union(data_series.index)) 103 return_data[identifier] = data_series 104 return_meta = concat([return_meta, cat_meta.loc[identifier]], axis=1) 105 106 return return_data, return_meta.T
Get specific ABS data series by their ABS catalogue and series identifiers.
Parameters
cat : str The ABS catalogue ID.
series_id : str | Sequence[str] An ABS series ID or a sequence of ABS series IDs.
url : str = "" The URL of an ABS landing page. Use this for discontinued series that are no longer in the ABS Time Series Directory. If provided, data is retrieved from this URL instead of looking up the catalogue number. Passed through to read_abs_cat().
**kwargs : Any Keyword arguments for the read_abs_series function, which are the same as the keyword arguments for the read_abs_cat function.
Returns
tuple[DataFrame, DataFrame] A tuple of two DataFrames, one for the primary data and one for the metadata.
Example
import readabs as ra
from pandas import DataFrame
cat_num = "6202.0" # The ABS labour force survey
unemployment_rate = "A84423050A"
seo = "62020001" # The ABS table name
data, meta = ra.read_abs_series(
cat=cat_num, series_id=unemployment_rate, single_excel_only=seo
)
171def read_rba_ocr(*, monthly: bool = True, **kwargs: Any) -> Series: # ignore_errors 172 """Read the Official Cash Rate (OCR) from the RBA website. 173 174 Return it in a pandas Series, with either a daily or monthly PeriodIndex, 175 depending on the value of the monthly parameter. The default is monthly. 176 177 Parameters 178 ---------- 179 monthly : bool = True 180 If True, then the data will be returned with a monthly PeriodIndex. 181 If False, then the data will be returned with a daily PeriodIndex. 182 **kwargs : Any 183 Additional keyword arguments. The only keyword argument that is used is ignore_errors. 184 ignore_errors : bool = False 185 If True, then any major errors encountered will be printed and the function 186 will return an empty Series. If False, then any major errors encountered 187 will raise an exception. 188 189 Returns 190 ------- 191 Series 192 The OCR data in a pandas Series, with an index of either daily or monthly Periods. 193 194 Examples 195 -------- 196 ```python 197 ocr = read_rba_ocr(monthly=True) 198 ``` 199 200 """ 201 # read the OCR table from the RBA website, make float and sort, name the series 202 rba, _rba_meta = read_rba_table("A2", **kwargs) # should have a daily PeriodIndex 203 ocr_series = rba.loc[lambda x: x.index >= "1990-08-02", "ARBAMPCNCRT"] 204 ocr = ocr_series.astype(float).sort_index() # pyright: ignore[reportAttributeAccessIssue] 205 ocr.name = "RBA Official Cash Rate" 206 207 # bring up to date 208 today = Period(Timestamp.today(), freq=cast("PeriodIndex", ocr.index).freqstr) 209 last_period = cast("Period", ocr.index[-1]) 210 if last_period < today: 211 ocr[today] = ocr.iloc[-1] 212 213 if not monthly: 214 # fill in missing days and return daily data 215 daily_index = period_range(start=ocr.index.min(), end=ocr.index.max(), freq="D") 216 return ocr.reindex(daily_index).ffill() 217 218 # convert to monthly data, keeping last value if duplicates in month 219 # fill in missing months 220 ocr.index = PeriodIndex(ocr.index, freq="M") 221 ocr = ocr[~ocr.index.duplicated(keep="last")] 222 monthly_index = period_range(start=ocr.index.min(), end=ocr.index.max(), freq="M") 223 return ocr.reindex(monthly_index, method="ffill")
Read the Official Cash Rate (OCR) from the RBA website.
Return it in a pandas Series, with either a daily or monthly PeriodIndex, depending on the value of the monthly parameter. The default is monthly.
Parameters
monthly : bool = True If True, then the data will be returned with a monthly PeriodIndex. If False, then the data will be returned with a daily PeriodIndex. **kwargs : Any Additional keyword arguments. The only keyword argument that is used is ignore_errors. ignore_errors : bool = False If True, then any major errors encountered will be printed and the function will return an empty Series. If False, then any major errors encountered will raise an exception.
Returns
Series The OCR data in a pandas Series, with an index of either daily or monthly Periods.
Examples
ocr = read_rba_ocr(monthly=True)
89def read_rba_table(table: str, **kwargs: Any) -> tuple[DataFrame, DataFrame]: # ignore_errors 90 """Read a table from the RBA website and return the actual data and meta data. 91 92 Returns the actual data and the meta data in a tuple of two DataFrames. 93 94 Parameters 95 ---------- 96 table : str 97 The table to read from the RBA website. 98 **kwargs : Any 99 Additional keyword arguments. 100 The only keyword argument that is used is ignore_errors. 101 ignore_errors : bool = False 102 If True, then any major errors encountered will be printed and the function 103 will return empty DataFrames. If False, then any major errors encountered 104 will raise an exception. 105 106 Returns 107 ------- 108 tuple[DataFrame, DataFrame] 109 The primary data and the meta data in a tuple of two DataFrames. 110 111 Examples 112 -------- 113 ```python 114 data, meta = read_rba_table("C1") 115 ``` 116 117 """ 118 # set-up 119 ignore_errors = kwargs.get("ignore_errors", False) 120 data, meta = DataFrame(), DataFrame() 121 122 # get the Excel file 123 excel = _get_excel_file(table, ignore_errors=ignore_errors, **kwargs) 124 if excel is None: 125 return data, meta 126 127 # read Excel file into DataFrame 128 try: 129 raw = read_excel(BytesIO(excel), header=None, index_col=None) 130 except Exception as e: 131 if ignore_errors: 132 print(f"Ignoring error: {e}") 133 return data, meta 134 raise 135 136 # extract the meta data 137 meta = raw.iloc[1:11, :].T.copy() 138 meta.columns = Index(meta.iloc[0]) 139 renamer = { 140 "Mnemonic": rm.id, 141 } # historical data is inconsistent 142 meta = meta.rename(columns=renamer) 143 meta = meta.iloc[1:, :] 144 meta.index = Index(meta[rm.id]) 145 meta[rm.table] = table 146 meta[rm.tdesc] = raw.iloc[0, 0] 147 meta = meta.dropna(how="all", axis=1) # drop columns with all NaNs 148 149 # extract the data 150 data = raw.iloc[10:, :].copy() 151 data.columns = Index(data.iloc[0]) 152 data = data.iloc[1:, :] 153 data.index = DatetimeIndex(data.iloc[:, 0]) 154 data = data.iloc[:, 1:] 155 data = data.dropna(how="all", axis=1) # drop columns with all NaNs 156 157 # can we make the index into a PeriodIndex? 158 days = data.index.to_series().diff(1).dropna().dt.days 159 if days.min() >= MONTHLY_MIN_DAYS and days.max() <= MONTHLY_MAX_DAYS: 160 data.index = PeriodIndex(data.index, freq="M") 161 elif days.min() >= QUARTERLY_MIN_DAYS and days.max() <= QUARTERLY_MAX_DAYS: 162 data.index = PeriodIndex(data.index, freq="Q") 163 elif days.min() >= YEARLY_MIN_DAYS and days.max() <= YEARLY_MAX_DAYS: 164 data.index = PeriodIndex(data.index, freq="Y") 165 else: 166 data.index = PeriodIndex(data.index, freq="D") 167 168 return data, meta
Read a table from the RBA website and return the actual data and meta data.
Returns the actual data and the meta data in a tuple of two DataFrames.
Parameters
table : str The table to read from the RBA website. **kwargs : Any Additional keyword arguments. The only keyword argument that is used is ignore_errors. ignore_errors : bool = False If True, then any major errors encountered will be printed and the function will return empty DataFrames. If False, then any major errors encountered will raise an exception.
Returns
tuple[DataFrame, DataFrame] The primary data and the meta data in a tuple of two DataFrames.
Examples
data, meta = read_rba_table("C1")
24def recalibrate( 25 data: DataT, 26 units: str, 27) -> tuple[DataT, str]: 28 """Recalibrate a Series or DataFrame so the data is in the range -1000 to 1000. 29 30 Change the name of the units to reflect the recalibration. 31 32 Note, DataT = TypeVar("DataT", Series, DataFrame). DataT is a constrained typevar. 33 If you provide a Series, you will get a Series back. If you provide a DataFrame, 34 you will get a DataFrame back. 35 36 Parameters 37 ---------- 38 data : Series or DataFrame 39 The data to recalibrate. 40 units : str 41 The units of the data. This string should be in the form of 42 "Number", "Thousands", "Millions", "Billions", etc. The units 43 should be in title case. 44 45 Returns 46 ------- 47 Series or DataFrame 48 The recalibrated data will be a Series if a Series was provided, 49 or a DataFrame if a DataFrame was provided. 50 51 Examples 52 -------- 53 ```python 54 from pandas import Series 55 from readabs import recalibrate 56 s = Series([1_000, 10_000, 100_000, 1_000_000]) 57 recalibrated, units = recalibrate(s, "$") 58 print(f"{recalibrated=}, {units=}") 59 ``` 60 61 """ 62 if not isinstance(data, (Series, DataFrame)): 63 raise TypeError("data must be a Series or DataFrame") 64 units, restore_name = _prepare_units(units) 65 flat_data = data.to_numpy().flatten() 66 flat_data, units = _recalibrate(flat_data, units) 67 68 if restore_name: 69 units = f"{restore_name} {units}" 70 for n in "numbers", "number": 71 if n in units: 72 units = units.replace(n, "").strip() 73 break 74 units = units.title() 75 76 result = data.__class__(flat_data.reshape(data.shape)) 77 result.index = data.index 78 # restore the column labels (DataFrame) or series name (Series); the 79 # isinstance checks narrow the constrained TypeVar so the attributes type-check 80 if isinstance(data, DataFrame) and isinstance(result, DataFrame): 81 result.columns = data.columns 82 elif isinstance(data, Series) and isinstance(result, Series): 83 result.name = data.name 84 return result, units
Recalibrate a Series or DataFrame so the data is in the range -1000 to 1000.
Change the name of the units to reflect the recalibration.
Note, DataT = TypeVar("DataT", Series, DataFrame). DataT is a constrained typevar. If you provide a Series, you will get a Series back. If you provide a DataFrame, you will get a DataFrame back.
Parameters
data : Series or DataFrame The data to recalibrate. units : str The units of the data. This string should be in the form of "Number", "Thousands", "Millions", "Billions", etc. The units should be in title case.
Returns
Series or DataFrame The recalibrated data will be a Series if a Series was provided, or a DataFrame if a DataFrame was provided.
Examples
from pandas import Series
from readabs import recalibrate
s = Series([1_000, 10_000, 100_000, 1_000_000])
recalibrated, units = recalibrate(s, "$")
print(f"{recalibrated=}, {units=}")
87def recalibrate_value(value: float, units: str) -> tuple[float, str]: 88 """Recalibrate a floating point value. 89 90 The value will be recalibrated so it is in the range -1000 to 1000. 91 The units will be changed to reflect the recalibration. 92 93 Parameters 94 ---------- 95 value : float 96 The value to recalibrate. 97 units : str 98 The units of the value. This string should be in the form of 99 "Number", "Thousands", "Millions", "Billions", etc. The units 100 should be in title case. 101 102 Returns 103 ------- 104 tuple[float, str] 105 A tuple containing the recalibrated value and the recalibrated units. 106 107 Examples 108 -------- 109 ```python 110 from readabs import recalibrate_value 111 recalibrated, units = recalibrate_value(10_000_000, "Thousand") 112 print(recalibrated, units) 113 ``` 114 115 """ 116 series = Series([value]) 117 output, units = recalibrate(series, units) 118 return output.to_numpy()[0], units
Recalibrate a floating point value.
The value will be recalibrated so it is in the range -1000 to 1000. The units will be changed to reflect the recalibration.
Parameters
value : float The value to recalibrate. units : str The units of the value. This string should be in the form of "Number", "Thousands", "Millions", "Billions", etc. The units should be in title case.
Returns
tuple[float, str] A tuple containing the recalibrated value and the recalibrated units.
Examples
from readabs import recalibrate_value
recalibrated, units = recalibrate_value(10_000_000, "Thousand")
print(recalibrated, units)
18def search_abs_meta( 19 meta: DataFrame, # sourced from read_abs_series() or read_abs_cat() 20 search_terms: dict[str, str], # {search_term: meta_data_column_name, ...} 21 *, 22 exact_match: bool = False, 23 regex: bool = False, 24 validate_unique: bool = False, # useful safety-net if you expect only one match 25 **kwargs: Unpack[VerboseArgs], 26) -> DataFrame: 27 """Extract from the ABS meta data those rows that match the search_terms. 28 29 Iteratively search the meta data one search_term at a time. 30 31 Parameters 32 ---------- 33 meta : DataFrame 34 A pandas DataFrame of metadata from the ABS 35 (via read_abs_cat() or read_abs_series()). 36 search_terms : dict[str, str] 37 A dictionary {search_phrase: meta_column_name, ...} of search terms. 38 Note: the search terms must be unique, as a dictionary cannot hold the 39 same search term to be applied to different columns. 40 exact_match : bool = False 41 Whether to match using == (exact) or .str.contains() (inexact). 42 regex : bool = False 43 Whether to use regular expressions in the search. 44 validate_unique : bool = False 45 Raise a ValueError if the search result is not unique. 46 **kwargs : Unpack[VerboseArgs] 47 Additional keyword arguments. The only keyword argument 48 that is used is verbose. Unknown keyword arguments are reported. 49 verbose : bool = False 50 Print additional information while searching; which can 51 be useful when diagnosing problems with search terms. 52 53 Returns 54 ------- 55 DataFrame 56 Returns a pandas DataFrame of matching rows (subseted from meta). 57 Note, The index for the returned meta data will always comprise ABS 58 series_ids. Duplicate indexes will be removed from the meta data 59 (ie. where the same ABS series appears in more than one table, this 60 function will only report the first match). 61 62 Metacol 63 ------- 64 Because the meta data is a DataFrame, the columns can be referenced by either 65 their full textual name, or by the short name defined in the metacol object. 66 For example, if metacol is imported as mc, to refer to the 67 `Data Item Description` column, the user can refer to it as mc.did. 68 69 Example 70 ------- 71 ```python 72 from readabs import metacol as mc # alias for the ABS meta data column names 73 from readabs import read_abs_cat, search_abs_meta 74 cat_num = "6202.0" # The ABS labour force survey 75 data, meta = read_abs_cat(cat_num) 76 search_terms = { 77 "Unemployment rate": mc.did, # the data item description 78 "Persons": mc.did, 79 "Seasonally Adjusted": mc.stype, 80 "Percent": mc.unit, 81 "62020001": mc.table, 82 } 83 rows = search_abs_meta(meta, search_terms, verbose=True) 84 print(rows) # should have three rows : FT/PT/All Unemployment rates 85 ``` 86 87 """ 88 # warn if invalid kwargs; the named parameters never reach kwargs, 89 # so checking against all the SearchArgs names only affects the message 90 check_kwargs(kwargs, "search_abs_meta", _SEARCH_KWARGS) 91 92 # get the verbose-flag from kwargs 93 verbose = kwargs.get("verbose", False) 94 95 # establish the starting point 96 meta_select = meta.copy() # preserve the original meta data 97 if verbose: 98 print(f"In search_abs_meta() {exact_match=} {regex=} {verbose=}") 99 print(f"In search_abs_meta() starting with {len(meta_select)} rows in the meta_data.") 100 101 # iteratively search 102 for phrase, column in search_terms.items(): 103 if verbose: 104 print(f"Searching {len(meta_select)}: term: {phrase} in-column: {column}") 105 106 pick_me = ( 107 (meta_select[column] == phrase) 108 if (exact_match or column == mc.table) 109 else meta_select[column].str.contains(phrase, regex=regex) 110 ) 111 meta_select = meta_select[pick_me] 112 if verbose: 113 print(f"In find_rows() have found {len(meta_select)}") 114 115 # search complete - check results - and return 116 meta_select.index = Index(meta_select[mc.id]) 117 meta_select = meta_select[~meta_select.index.duplicated(keep="first")] 118 119 if verbose: 120 print(f"Final selection is {len(meta_select)} rows.") 121 122 elif len(meta_select) == 0: 123 print("Nothing selected?") 124 125 if validate_unique and len(meta_select) != 1: 126 raise ValueError("The selected meta data should only contain one row.") 127 128 return meta_select
Extract from the ABS meta data those rows that match the search_terms.
Iteratively search the meta data one search_term at a time.
Parameters
meta : DataFrame A pandas DataFrame of metadata from the ABS (via read_abs_cat() or read_abs_series()). search_terms : dict[str, str] A dictionary {search_phrase: meta_column_name, ...} of search terms. Note: the search terms must be unique, as a dictionary cannot hold the same search term to be applied to different columns. exact_match : bool = False Whether to match using == (exact) or .str.contains() (inexact). regex : bool = False Whether to use regular expressions in the search. validate_unique : bool = False Raise a ValueError if the search result is not unique. **kwargs : Unpack[VerboseArgs] Additional keyword arguments. The only keyword argument that is used is verbose. Unknown keyword arguments are reported. verbose : bool = False Print additional information while searching; which can be useful when diagnosing problems with search terms.
Returns
DataFrame Returns a pandas DataFrame of matching rows (subseted from meta). Note, The index for the returned meta data will always comprise ABS series_ids. Duplicate indexes will be removed from the meta data (ie. where the same ABS series appears in more than one table, this function will only report the first match).
Metacol
Because the meta data is a DataFrame, the columns can be referenced by either
their full textual name, or by the short name defined in the metacol object.
For example, if metacol is imported as mc, to refer to the
Data Item Description column, the user can refer to it as mc.did.
Example
from readabs import metacol as mc # alias for the ABS meta data column names
from readabs import read_abs_cat, search_abs_meta
cat_num = "6202.0" # The ABS labour force survey
data, meta = read_abs_cat(cat_num)
search_terms = {
"Unemployment rate": mc.did, # the data item description
"Persons": mc.did,
"Seasonally Adjusted": mc.stype,
"Percent": mc.unit,
"62020001": mc.table,
}
rows = search_abs_meta(meta, search_terms, verbose=True)
print(rows) # should have three rows : FT/PT/All Unemployment rates
330def select(sources: Iterable[Source], *, require_same_units: bool = True) -> list[Series]: 331 """Select a series for each ``(data, meta, selector)`` — the iterable in, iterable out. 332 333 The composable selection primitive: takes the iterable of ``(data, meta, 334 selector)`` sources and returns the matching list of Series, ready to hand to 335 :func:`splice` (directly, or after a per-series transform). Each selection 336 goes through ``readabs.find_abs_id`` with ``validate_unique=True``, which 337 de-duplicates on Series ID first — so a selector matching the same series in 338 several tables resolves cleanly, while one matching two genuinely different 339 series raises rather than guessing. 340 341 Parameters 342 ---------- 343 sources 344 Iterable of ``(data, meta, selector)``: 345 346 - ``data`` — ``dict[table_name, DataFrame]`` from ``read_abs_cat``. 347 - ``meta`` — the matching metadata DataFrame. 348 - ``selector`` — ``{search_value: meta_column}`` for ``find_abs_id``, e.g. 349 ``{"Index Numbers ; All groups CPI ; Australia ;": mc.did, 350 "Index Numbers": mc.unit, "Quarter": mc.freq}``; or a bare ABS Series 351 ID string (e.g. ``"A2325846C"``), matched exactly. 352 require_same_units 353 If ``True`` (default) **raise** when the selected series do not all share 354 the same ABS unit — units must cohere to be spliced. Set ``False`` when 355 you deliberately select different-unit series together (e.g. two counts 356 and a rate that you will combine yourself). 357 358 Returns 359 ------- 360 list[Series] 361 One Series per source, each named by its Series ID with its ABS unit in 362 ``series.attrs["unit"]``. Unpack it (``a, b = select([...])``), map a 363 transform over it, or pass it straight to :func:`splice`. A later 364 transform drops the unit attr — correctly, since the unit is then no 365 longer the ABS one. 366 367 Raises 368 ------ 369 ValueError 370 If ``require_same_units`` and the selected series carry mixed units. 371 372 """ 373 segments = [select_one(data, meta, selector) for data, meta, selector in sources] 374 if require_same_units: 375 units = [str(s.attrs.get("unit", "")) for s in segments] 376 if len(set(units)) > 1: 377 detail = ", ".join(f"{s.name}={u!r}" for s, u in zip(segments, units, strict=True)) 378 raise ValueError( 379 f"select: selected series have mismatched units ({detail}). Pass " 380 f"require_same_units=False to select different-unit series together." 381 ) 382 return segments
Select a series for each (data, meta, selector) — the iterable in, iterable out.
The composable selection primitive: takes the iterable of (data, meta,
selector) sources and returns the matching list of Series, ready to hand to
splice() (directly, or after a per-series transform). Each selection
goes through readabs.find_abs_id with validate_unique=True, which
de-duplicates on Series ID first — so a selector matching the same series in
several tables resolves cleanly, while one matching two genuinely different
series raises rather than guessing.
Parameters
sources
Iterable of (data, meta, selector):
- ``data`` — ``dict[table_name, DataFrame]`` from ``read_abs_cat``.
- ``meta`` — the matching metadata DataFrame.
- ``selector`` — ``{search_value: meta_column}`` for ``find_abs_id``, e.g.
``{"Index Numbers ; All groups CPI ; Australia ;": mc.did,
"Index Numbers": mc.unit, "Quarter": mc.freq}``; or a bare ABS Series
ID string (e.g. ``"A2325846C"``), matched exactly.
require_same_units
If True (default) raise when the selected series do not all share
the same ABS unit — units must cohere to be spliced. Set False when
you deliberately select different-unit series together (e.g. two counts
and a rate that you will combine yourself).
Returns
list[Series]
One Series per source, each named by its Series ID with its ABS unit in
series.attrs["unit"]. Unpack it (a, b = select([...])), map a
transform over it, or pass it straight to splice(). A later
transform drops the unit attr — correctly, since the unit is then no
longer the ABS one.
Raises
ValueError
If require_same_units and the selected series carry mixed units.
385def select_and_splice( 386 sources: Iterable[Source], 387 *, 388 target: str | None = None, 389 rebase: bool = False, 390 agg: str = "mean", 391 output: str | None = None, 392 fill: Literal["ffill", "interpolate"] | None = None, 393 name: str | None = None, 394 require_same_units: bool = True, 395) -> tuple[Series, str, DataFrame]: 396 """Select one series per source and :func:`splice` them — the no-transform case. 397 398 Sugar for ``splice(select(sources))`` with a unit guard. When 399 you need a transform *between* selecting and splicing (e.g. a growth rate), 400 compose :func:`select` and :func:`splice` directly instead — that is the whole 401 reason :func:`select` is exposed separately. 402 403 Parameters 404 ---------- 405 sources 406 Ordered iterable of ``(data, meta, selector)``, **highest priority 407 first** (same priority rule as :func:`splice`): 408 409 - ``data`` — ``dict[table_name, DataFrame]`` from ``read_abs_cat``. 410 - ``meta`` — the matching metadata DataFrame. 411 - ``selector`` — ``{search_value: meta_column}`` for ``find_abs_id``, 412 e.g. ``{"Index Numbers ; All groups CPI ; Australia ;": mc.did, 413 "Index Numbers": mc.unit, "Quarter": mc.freq}``; or a bare ABS Series 414 ID string (e.g. ``"A2325846C"``), matched exactly. In the common case 415 the only thing differing between two sources is the frequency, so a 416 shared *base* selector composes with ``base | {"Quarter": mc.freq}``. 417 target, rebase, agg, output, fill, name 418 Passed straight through to :func:`splice`. 419 require_same_units 420 Forwarded to :func:`select`: if ``True`` (default) raise when the 421 selected segments carry mixed units; ``False`` overrides (the result is 422 then labelled with the highest-priority segment's unit). 423 424 Returns 425 ------- 426 tuple[Series, str, DataFrame] 427 The spliced series, its unit (the highest-priority segment's unit), and 428 the :func:`splice` join report, augmented with ``series_id`` and 429 ``unit`` columns recording what each segment resolved to. 430 431 """ 432 segments = select(sources, require_same_units=require_same_units) 433 units = [str(s.attrs.get("unit", "")) for s in segments] 434 435 result, report = splice(segments, target=target, rebase=rebase, agg=agg, output=output, fill=fill, name=name) 436 # Audit trail: which Series ID / unit did each reported (lower-priority) segment use? 437 if len(report): 438 seg = [int(i) for i in report["segment"]] 439 report.insert(1, "series_id", [str(segments[i].name) for i in seg]) 440 report.insert(2, "unit", [units[i] for i in seg]) 441 return result, units[0], report
Select one series per source and splice() them — the no-transform case.
Sugar for splice(select(sources)) with a unit guard. When
you need a transform between selecting and splicing (e.g. a growth rate),
compose select() and splice() directly instead — that is the whole
reason select() is exposed separately.
Parameters
sources
Ordered iterable of (data, meta, selector), highest priority
first (same priority rule as splice()):
- ``data`` — ``dict[table_name, DataFrame]`` from ``read_abs_cat``.
- ``meta`` — the matching metadata DataFrame.
- ``selector`` — ``{search_value: meta_column}`` for ``find_abs_id``,
e.g. ``{"Index Numbers ; All groups CPI ; Australia ;": mc.did,
"Index Numbers": mc.unit, "Quarter": mc.freq}``; or a bare ABS Series
ID string (e.g. ``"A2325846C"``), matched exactly. In the common case
the only thing differing between two sources is the frequency, so a
shared *base* selector composes with ``base | {"Quarter": mc.freq}``.
target, rebase, agg, output, fill, name
Passed straight through to splice().
require_same_units
Forwarded to select(): if True (default) raise when the
selected segments carry mixed units; False overrides (the result is
then labelled with the highest-priority segment's unit).
Returns
tuple[Series, str, DataFrame]
The spliced series, its unit (the highest-priority segment's unit), and
the splice() join report, augmented with series_id and
unit columns recording what each segment resolved to.
305def select_one(data: dict[str, DataFrame], meta: DataFrame, selector: dict[str, str] | str) -> Series: 306 """Select the single Series for one ``(data, meta, selector)`` — the single-source wrapper. 307 308 Convenience for the common one-selector case; equivalent to 309 ``select([(data, meta, selector)])[0]``. The *selector* is either a 310 ``{search_value: meta_column}`` dict for ``find_abs_id``, or a bare ABS 311 Series ID string, matched exactly against the metadata's Series ID column. 312 Returns the Series named by its Series ID, with its ABS unit on 313 ``.attrs["unit"]``. 314 """ 315 if isinstance(selector, str): 316 # A bare Series ID — same find_abs_id machinery, but exact-match on the 317 # Series ID column so one ID cannot substring-match another. 318 try: 319 table, series_id, unit = find_abs_id(meta, {selector: mc.id}, exact_match=True, validate_unique=True) 320 except ValueError as exc: 321 raise ValueError(f"select: series ID {selector!r} not found in the supplied metadata.") from exc 322 else: 323 table, series_id, unit = find_abs_id(meta, selector, validate_unique=True) 324 s = data[table][series_id].copy() 325 s.name = series_id 326 s.attrs["unit"] = str(unit) 327 return s
Select the single Series for one (data, meta, selector) — the single-source wrapper.
Convenience for the common one-selector case; equivalent to
select([(data, meta, selector)])[0]. The selector is either a
{search_value: meta_column} dict for find_abs_id, or a bare ABS
Series ID string, matched exactly against the metadata's Series ID column.
Returns the Series named by its Series ID, with its ABS unit on
.attrs["unit"].
180def splice( 181 segments: Iterable[Series], 182 *, 183 target: str | None = None, 184 rebase: bool = False, 185 agg: str = "mean", 186 output: str | None = None, 187 fill: Literal["ffill", "interpolate"] | None = None, 188 name: str | None = None, 189) -> tuple[Series, DataFrame]: 190 """Splice mixed-frequency *segments* into one series, highest priority first. 191 192 Parameters 193 ---------- 194 segments 195 Ordered list of pandas Series (PeriodIndex or DatetimeIndex). The 196 first is highest priority: it wins where periods overlap and (when 197 ``rebase`` is on) sets the level everything else is rebased to. 198 target 199 Common-grid frequency (e.g. ``"M"``, ``"Q-DEC"``). Defaults to the 200 finest frequency present (anchor clashes step one rank finer). 201 rebase 202 Off by default — segments are coalesced at their **raw** levels, with no 203 silent transformation of your data. Set ``True`` to *multiplicatively* 204 rescale each lower-priority segment to the running result's level before 205 coalescing. Rebasing assumes **ratio-scale** inputs (meaningful zero, 206 proportional discrepancy between segments) — splicing index series on 207 different base periods (CPI, price/volume indices) is the case that 208 needs it. It is wrong for zero-crossing series (rates, balances) or 209 additive level breaks, and it *invents* a correction when same-unit 210 segments already agree — which is why it is opt-in. A non-finite or 211 non-positive factor raises. See the module docstring's *rebase* step. 212 agg 213 Aggregator used when a segment is finer than the grid (or when 214 downsampling to *output*). ``"mean"`` for index levels; use ``"sum"`` 215 for flows. 216 output 217 Optional final frequency to resample the spliced result to. 218 fill 219 Optional gap fill. By default (``None``) the result contains only the 220 periods that actually have data — no NaN rows are inserted for the gaps 221 a coarse segment leaves on a finer grid, and nothing is interpolated. 222 ``"ffill"`` or ``"interpolate"`` densify the result onto the full grid 223 first and then fill. 224 name 225 Name for the result series (defaults to the first segment's name). 226 227 Returns 228 ------- 229 tuple[Series, DataFrame] 230 The spliced series and a one-row-per-junction report. 231 232 """ 233 segments = list(segments) 234 if not segments: 235 raise ValueError("splice() needs at least one segment.") 236 237 grid = target or _pick_target(segments) 238 on_grid = [_to_grid(s, grid, agg) for s in segments] 239 240 result = on_grid[0].copy() 241 rows: list[dict[str, object]] = [] 242 for i, seg in enumerate(on_grid[1:], start=1): 243 if rebase: 244 factor, method, n, lo, hi = _rebase_factor(result, seg) 245 # Multiplicative rebasing assumes ratio-scale inputs. A non-finite 246 # factor (near-zero denominator) or a non-positive one (the overlap 247 # means have opposite signs, which would flip the back-history) means 248 # the data is not ratio-scale — fail loud rather than ship it. A 249 # large *magnitude* is fine: a legitimate base-period difference can 250 # need a 50x factor, so only sign and finiteness are guarded. 251 if not (math.isfinite(factor) and factor > 0): 252 raise ValueError( 253 f"splice: rebase factor for segment {i} ('{seg.name}') is {factor} over " 254 f"{lo}..{hi}. Multiplicative rebasing needs ratio-scale inputs (meaningful " 255 f"zero, proportional discrepancy); a non-finite or non-positive factor means " 256 f"the segments cross zero or differ additively. Pass rebase=False to coalesce " 257 f"raw levels instead." 258 ) 259 else: 260 factor, method, n, lo, hi = 1.0, "off", 0, None, None 261 seg_rebased = seg * factor 262 rows.append( 263 { 264 "segment": i, 265 "name": str(seg.name), 266 "freq_in": str(_pidx(segments[i]).freqstr), 267 "method": method, 268 "overlap_n": n, 269 "window_start": str(lo) if lo is not None else "", 270 "window_end": str(hi) if hi is not None else "", 271 "factor": round(factor, 6), 272 "fills_from": str(seg.dropna().index.min()), 273 } 274 ) 275 result = result.combine_first(seg_rebased) 276 277 # By default keep only the periods that actually carry data: do NOT reindex 278 # onto a dense grid (which would manufacture NaN for the gaps a coarse 279 # back-history leaves on a finer grid) and do NOT interpolate. A long-run 280 # series therefore stays sparse where it is old and coarse, and plots as one 281 # continuous line with no holes and no invented points. 282 result = result.dropna().sort_index() 283 284 if output and output != grid: 285 result = _to_grid(result, output, agg).dropna().sort_index() 286 grid = output 287 288 if fill in ("ffill", "interpolate") and len(result): 289 # Explicit opt-in: densify onto the full grid, then fill. 290 full = pd.period_range(result.index.min(), result.index.max(), freq=grid) 291 result = result.reindex(full) 292 result = result.ffill() if fill == "ffill" else result.interpolate() 293 294 result.name = name or str(segments[0].name) 295 report = DataFrame(rows) 296 return result, report
Splice mixed-frequency segments into one series, highest priority first.
Parameters
segments
Ordered list of pandas Series (PeriodIndex or DatetimeIndex). The
first is highest priority: it wins where periods overlap and (when
rebase is on) sets the level everything else is rebased to.
target
Common-grid frequency (e.g. "M", "Q-DEC"). Defaults to the
finest frequency present (anchor clashes step one rank finer).
rebase
Off by default — segments are coalesced at their raw levels, with no
silent transformation of your data. Set True to multiplicatively
rescale each lower-priority segment to the running result's level before
coalescing. Rebasing assumes ratio-scale inputs (meaningful zero,
proportional discrepancy between segments) — splicing index series on
different base periods (CPI, price/volume indices) is the case that
needs it. It is wrong for zero-crossing series (rates, balances) or
additive level breaks, and it invents a correction when same-unit
segments already agree — which is why it is opt-in. A non-finite or
non-positive factor raises. See the module docstring's rebase step.
agg
Aggregator used when a segment is finer than the grid (or when
downsampling to output). "mean" for index levels; use "sum"
for flows.
output
Optional final frequency to resample the spliced result to.
fill
Optional gap fill. By default (None) the result contains only the
periods that actually have data — no NaN rows are inserted for the gaps
a coarse segment leaves on a finer grid, and nothing is interpolated.
"ffill" or "interpolate" densify the result onto the full grid
first and then fill.
name
Name for the result series (defaults to the first segment's name).
Returns
tuple[Series, DataFrame] The spliced series and a one-row-per-junction report.