readabs.read_abs_cat

Download timeseries data from the Australian Bureau of Statistics.

Download timeseries data from the Australian Bureau of Statistics (ABS) for a specified ABS catalogue identifier.

  1"""Download *timeseries* data from the Australian Bureau of Statistics.
  2
  3Download timeseries data from the Australian Bureau of Statistics (ABS)
  4for a specified ABS catalogue identifier.
  5"""
  6
  7import calendar
  8from functools import cache
  9from pathlib import Path
 10from typing import Any, Unpack
 11
 12import pandas as pd
 13from pandas import DataFrame
 14
 15from readabs.abs_meta_data import metacol
 16from readabs.grab_abs_url import grab_abs_url, grab_abs_zip
 17from readabs.read_support import HYPHEN, ReadArgs
 18
 19# Constants
 20MAX_DATETIME_CHARS = 20
 21TABLE_DESC_ROW = 4
 22TABLE_DESC_COL = 1
 23
 24
 25# --- functions ---
 26# - public -
 27@cache  # minimise slowness for any repeat business
 28def read_abs_cat(
 29    cat: str,
 30    **kwargs: Unpack[ReadArgs],
 31) -> tuple[dict[str, DataFrame], DataFrame]:
 32    """For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames.
 33
 34    This function returns the complete ABS Catalogue information as a
 35    python dictionary of pandas DataFrames, as well as the associated metadata
 36    in a separate DataFrame. The function automates the collection of zip and
 37    excel files from the ABS website. If necessary, these files are downloaded,
 38    and saved into a cache directory. The files are then parsed to extract time
 39    series data, and the associated metadata.
 40
 41    By default, the cache directory is `./.readabs_cache/`. You can change the
 42    default directory name by setting the shell environment variable
 43    `READABS_CACHE_DIR` with the name of the preferred directory.
 44
 45    Parameters
 46    ----------
 47    cat : str
 48        The ABS Catalogue Number for the data to be downloaded and made
 49        available by this function. This argument must be specified in the
 50        function call.
 51
 52    **kwargs : Unpack[ReadArgs]
 53        The following parameters may be passed as optional keyword arguments.
 54
 55    url : str = ""
 56        The URL of an ABS landing page. Use this for discontinued series
 57        that are no longer in the ABS Time Series Directory. If provided,
 58        data will be retrieved from this URL instead of looking up the
 59        catalogue number. Example:
 60        `read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")`
 61
 62    keep_non_ts : bool = False
 63        A flag for whether to keep the non-time-series tables
 64        that might form part of an ABS catalogue item. Normally, the
 65        non-time-series information is ignored, and not made available to
 66        the user.
 67
 68    history : str = ""
 69        Provide a month-year string to extract historical ABS data.
 70        For example, you can set history="dec-2023" to the get the ABS data
 71        for a catalogue identifier that was originally published in respect
 72        of Q4 of 2023. Note: not all ABS data sources are structured so that
 73        this technique works in every case; but most are.
 74
 75    verbose : bool = False
 76        Setting this to true may help diagnose why something
 77        might be going wrong with the data retrieval process.
 78
 79    ignore_errors : bool = False
 80        Normally, this function will cease downloading when
 81        an error in encountered. However, sometimes the ABS website has
 82        malformed links, and changing this setting is necessitated. (Note:
 83        if you drop a message to the ABS, they will usually fix broken
 84        links with a business day).
 85
 86    get_zip : bool = True
 87        Download the excel files in .zip files.
 88
 89    get_excel_if_no_zip : bool = True
 90        Only try to download .xlsx files if there are no zip
 91        files available to be downloaded. Only downloading individual excel
 92        files when there are no zip files to download can speed up the
 93        download process.
 94
 95    get_excel : bool = False
 96        The default value means that excel files are not
 97        automatically download. Note: at least one of `get_zip`,
 98        `get_excel_if_no_zip`, or `get_excel` must be true. For most ABS
 99        catalogue items, it is sufficient to just download the one zip
100        file. But note, some catalogue items do not have a zip file.
101        Others have quite a number of zip files.
102
103    single_excel_only : str = ""
104        If this argument is set to a table name (without the
105        .xlsx extension), only that excel file will be downloaded. If
106        set, and only a limited subset of available data is needed,
107        this can speed up download times significantly. Note: overrides
108        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`.
109
110    selected_excel : tuple[str, ...] = ()
111        If set to a tuple of table names (without the .xlsx extension),
112        only those excel files will be downloaded. Useful when several
113        specific tables are needed and downloading the full zip would
114        be wasteful. Example:
115        `selected_excel=("62020001", "62020017", "62020X28")`.
116        Must be a tuple (not a list) because `read_abs_cat` uses an
117        internal cache that requires hashable arguments. Note: overrides
118        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`
119        when at least one matching file is found.
120
121    single_zip_only : str = ""
122        If this argument is set to a zip file name (without
123        the .zip extension), only that zip file will be downloaded.
124        If set, and only a limited subset of available data is needed,
125        this can speed up download times significantly. Note: overrides
126        `get_zip`, `get_excel_if_no_zip`, and `get_excel`.
127
128    cache_only : bool = False
129        If set to True, this function will only access
130        data that has been previously cached. Normally, the function
131        checks the date of the cache data against the date of the data
132        on the ABS website, before deciding whether the ABS has fresher
133        data that needs to be downloaded to the cache.
134
135    zip_file: str | Path = ""
136        If set to a specific zip file name (with or without the .zip
137        extension), this function will only extract data from that zip file
138        on the local file system. This may be useful for debugging purposes.
139
140    Returns
141    -------
142    tuple[dict[str, DataFrame], DataFrame]
143        The function returns a tuple of two items. The first item is a
144        python dictionary of pandas DataFrames (which is the primary data
145        associated with the ABS catalogue item). The second item is a
146        DataFrame of ABS metadata for the ABS collection.
147
148        Note:
149        You can retrieve non-timeseries data using the grab_abs_url()
150        function. That takes the URL for the ABS landing page for the ABS
151        collection you are interested in. The read_abs_cat function is for
152        ABS catalogue identifiers which are timeseries data, for which the
153        metadata can be extracted.
154
155    Example
156    -------
157
158    ```python
159    import readabs as ra
160    from pandas import DataFrame
161    cat_num = "6202.0"  # The ABS labour force survey
162    data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num)
163    abs_dict, meta = data
164    ```
165
166    """
167    # --- get the time series data ---
168    zip_file = kwargs.get("zip_file")
169    raw_abs_dict = (
170        grab_abs_zip(zip_file, **kwargs) if zip_file else grab_abs_url(cat=cat, **kwargs)
171    )
172    response = _get_time_series_data(cat, raw_abs_dict, **kwargs)
173
174    if not response:
175        response = {}, DataFrame()
176
177    return response  # dictionary of DataFrames, and a DataFrame of metadata
178
179
180# - private -
181def _get_time_series_data(
182    cat: str,
183    abs_dict: dict[str, DataFrame],
184    **kwargs: Any,  # keep_non_ts, verbose, ignore_errors
185) -> tuple[dict[str, DataFrame], DataFrame]:
186    """Extract the time series data for a specific ABS catalogue identifier."""
187    # --- set up ---
188    cat = "<catalogue number missing>" if not cat.strip() else cat.strip()
189    new_dict: dict[str, DataFrame] = {}
190    meta_data = DataFrame()
191
192    # --- group the sheets and iterate over these groups
193    long_groups = _group_sheets(abs_dict)
194    for table, sheets in long_groups.items():
195        args = {
196            "cat": cat,
197            "from_dict": abs_dict,
198            "table": table,
199            "long_sheets": sheets,
200        }
201        new_dict, meta_data = _capture(new_dict, meta_data, args, **kwargs)
202    return new_dict, meta_data
203
204
205def _copy_raw_sheets(
206    from_dict: dict[str, DataFrame],
207    long_sheets: list[str],
208    to_dict: dict[str, DataFrame],
209    *,
210    keep_non_ts: bool,
211) -> dict[str, DataFrame]:
212    """Copy the raw sheets across to the final dictionary.
213
214    Used if the data is not in a timeseries format, and keep_non_ts
215    flag is set to True. Returns an updated final dictionary.
216    """
217    if not keep_non_ts:
218        return to_dict
219
220    for sheet in long_sheets:
221        if sheet in from_dict:
222            to_dict[sheet] = from_dict[sheet]
223        else:
224            # should not happen
225            raise ValueError(f"Glitch: Sheet {sheet} not found in the data.")
226    return to_dict
227
228
229def _capture(
230    to_dict: dict[str, DataFrame],
231    meta_data: DataFrame,
232    args: dict[str, Any],
233    **kwargs: Any,  # keep_non_ts, ignore_errors
234) -> tuple[dict[str, DataFrame], DataFrame]:
235    """Capture the time series data and meta data from an Excel file.
236
237    For a specific Excel file, capture *both* the time series data
238    from the ABS data files as well as the meta data. These data are
239    added to the input 'to_dict' and 'meta_data' respectively, and
240    the combined results are returned as a tuple.
241    """
242    # --- step 0: set up ---
243    keep_non_ts: bool = kwargs.get("keep_non_ts", False)
244    ignore_errors: bool = kwargs.get("ignore_errors", False)
245
246    # --- step 1: capture the meta data ---
247    short_names = [x.split(HYPHEN, 1)[1] for x in args["long_sheets"]]
248    if "Index" not in short_names:
249        print(f"Table {args['table']} has no 'Index' sheet.")
250        to_dict = _copy_raw_sheets(args["from_dict"], args["long_sheets"], to_dict, keep_non_ts=keep_non_ts)
251        return to_dict, meta_data
252    index = short_names.index("Index")
253
254    index_sheet = args["long_sheets"][index]
255    this_meta = _capture_meta(args["cat"], args["from_dict"], index_sheet)
256    if this_meta.empty:
257        to_dict = _copy_raw_sheets(args["from_dict"], args["long_sheets"], to_dict, keep_non_ts=keep_non_ts)
258        return to_dict, meta_data
259
260    meta_data = pd.concat([meta_data, this_meta], axis=0)
261
262    # --- step 2: capture the actual time series data ---
263    data = _capture_data(meta_data, args["from_dict"], args["long_sheets"], **kwargs)
264    if len(data):
265        to_dict[args["table"]] = data
266    else:
267        # a glitch: we have the metadata but not the actual data
268        error = f"Unexpected: {args['table']} has no actual data."
269        if not ignore_errors:
270            raise ValueError(error)
271        print(error)
272        to_dict = _copy_raw_sheets(args["from_dict"], args["long_sheets"], to_dict, keep_non_ts=keep_non_ts)
273
274    return to_dict, meta_data
275
276
277def _capture_data(
278    abs_meta: DataFrame,
279    from_dict: dict[str, DataFrame],
280    long_sheets: list[str],
281    **kwargs: Any,  # verbose
282) -> DataFrame:
283    """Take a list of ABS data sheets and stitch them into a DataFrame.
284
285    Find the DataFrames for those sheets in the from_dict, and stitch them
286    into a single DataFrame with an appropriate PeriodIndex.
287    """
288    # --- step 0: set up ---
289    verbose: bool = kwargs.get("verbose", False)
290    merged_data = DataFrame()
291    header_row: int = 8
292
293    # --- step 1: capture the time series data ---
294    # identify the data sheets in the list of all sheets from Excel file
295    data_sheets = [x for x in long_sheets if x.split(HYPHEN, 1)[1].startswith("Data")]
296
297    for sheet_name in data_sheets:
298        if verbose:
299            print(f"About to cature data from {sheet_name=}")
300
301        # --- capture just the data, nothing else
302        sheet_data = from_dict[sheet_name].copy()
303
304        # get the columns
305        header = sheet_data.iloc[header_row]
306        sheet_data.columns = pd.Index(header)
307        sheet_data = sheet_data[(header_row + 1) :]
308
309        # get the row indexes
310        sheet_data = _index_to_period(sheet_data, sheet_name, abs_meta, verbose=verbose)
311
312        # --- merge data into a single dataframe
313        if len(merged_data) == 0:
314            merged_data = sheet_data
315        else:
316            merged_data = merged_data.merge(
317                right=sheet_data,
318                how="outer",
319                left_index=True,
320                right_index=True,
321                suffixes=("", ""),
322            )
323
324    # --- step 2 - final tidy-ups
325    # remove NA rows
326    merged_data = merged_data.dropna(how="all")
327    # check for NA columns - rarely happens
328    # Note: these empty columns are not removed,
329    # but it is useful to know they are there
330    if merged_data.isna().all().any() and verbose:
331        na_cols = merged_data.columns[merged_data.isna().all()]
332        print(f"Caution: These columns are all NA: {list(na_cols)}")
333
334    # check for duplicate columns - should not happen
335    # Note: these duplicate columns are removed
336    duplicates = merged_data.columns.duplicated()
337    if duplicates.any():
338        if verbose:
339            dup_table = abs_meta[metacol.table].iloc[0]
340            print(f"Note: duplicates removed from {dup_table}: " + f"{merged_data.columns[duplicates]}")
341        merged_data = merged_data.loc[:, ~duplicates].copy()
342
343    # make the data all floats.
344    return merged_data.astype(float).sort_index()
345
346
347def _index_to_period(sheet_data: DataFrame, sheet_name: str, abs_meta: DataFrame, *, verbose: bool) -> DataFrame:
348    """Convert the index of a DataFrame to a PeriodIndex."""
349    index_column = sheet_data[sheet_data.columns[0]].astype(str)
350    sheet_data = sheet_data.drop(sheet_data.columns[0], axis=1)
351    long_row_names = index_column.str.len() > MAX_DATETIME_CHARS  # 19 chars in datetime str
352    if verbose and long_row_names.any():
353        print(f"You may need to check index column for {sheet_name}")
354    index_column = index_column.loc[~long_row_names]
355    sheet_data = sheet_data.loc[~long_row_names]
356
357    proposed_index = pd.to_datetime(index_column)
358
359    # get the correct period index
360    short_name = sheet_name.split(HYPHEN, 1)[0]
361    series_id = sheet_data.columns[0]
362    freq_value = abs_meta[abs_meta[metacol.table] == short_name].loc[series_id, metacol.freq]
363    freq = str(freq_value).upper().strip()[0]
364    freq = "Y" if freq == "A" else freq  # pandas prefers yearly
365    freq = "Q" if freq == "B" else freq  # treat Biannual as quarterly
366    if freq not in ("Y", "Q", "M", "D"):
367        print(f"Check the frequency of the data in sheet: {sheet_name}")
368
369    # create an appropriate period index
370    if freq:
371        if freq in ("Q", "Y"):
372            month = str(calendar.month_abbr[proposed_index.dt.month.max()]).upper()
373            freq = f"{freq}-{month}"
374        sheet_data.index = pd.PeriodIndex(proposed_index, freq=freq)
375    else:
376        raise ValueError(f"With sheet {sheet_name} could not determime PeriodIndex")
377
378    return sheet_data
379
380
381def _capture_meta(
382    cat: str,
383    from_dict: dict[str, DataFrame],
384    index_sheet: str,
385) -> DataFrame:
386    """Capture the metadata from the Index sheet of an ABS excel file.
387
388    Returns a DataFrame specific to the current excel file.
389    Returning an empty DataFrame, means that the meta data could not
390    be identified. Meta data for each ABS data item is organised by row.
391    """
392    # --- step 0: set up ---
393    frame = from_dict[index_sheet]
394
395    # --- step 1: check if the metadata is present in the right place ---
396    # Unfortunately, the header for some of the 3401.0
397    #                spreadsheets starts on row 10
398    starting_rows = 8, 9, 10
399    required = metacol.did, metacol.id, metacol.stype, metacol.unit
400    required_set = set(required)
401
402    header_row = None
403    header_columns = None
404    for row in starting_rows:
405        columns = frame.iloc[row]
406        if required_set.issubset(set(columns)):
407            header_row = row
408            header_columns = columns
409            break
410
411    if header_row is None or header_columns is None:
412        print(f"Table has no metadata in sheet {index_sheet}.")
413        return DataFrame()
414
415    # --- step 2: capture the metadata ---
416    file_meta = frame.iloc[header_row + 1 :].copy()
417    file_meta.columns = pd.Index(header_columns)
418
419    # make damn sure there are no rogue white spaces
420    for col in required:
421        file_meta[col] = file_meta[col].str.strip()
422
423    # remove empty columns and rows
424    file_meta = file_meta.dropna(how="all", axis=1).dropna(how="all", axis=0)
425
426    # populate the metadata
427    file_meta[metacol.table] = index_sheet.split(HYPHEN, 1)[0]
428    tab_desc_value = frame.iloc[TABLE_DESC_ROW, TABLE_DESC_COL]
429    tab_desc = str(tab_desc_value).split(".", 1)[-1].strip()
430    file_meta[metacol.tdesc] = tab_desc
431    file_meta[metacol.cat] = cat
432
433    # drop last row - should just be copyright statement
434    file_meta = file_meta.iloc[:-1]
435
436    # set the index to the series_id
437    file_meta.index = pd.Index(file_meta[metacol.id])
438
439    return file_meta
440
441
442def _group_sheets(
443    abs_dict: dict[str, DataFrame],
444) -> dict[str, list[str]]:
445    """Group the sheets from an Excel file."""
446    keys = list(abs_dict.keys())
447    long_pairs = [(x.split(HYPHEN, 1)[0], x) for x in keys]
448
449    def group(p_list: list[tuple[str, str]]) -> dict[str, list[str]]:
450        groups: dict[str, list[str]] = {}
451        for x, y in p_list:
452            if x not in groups:
453                groups[x] = []
454            groups[x].append(y)
455        return groups
456
457    return group(long_pairs)
458
459
460# --- initial testing ---
461if __name__ == "__main__":
462
463    def simple_test() -> None:
464        """Test the read_abs_cat function."""
465        # ABS Catalogue ID 8731.0 has a mix of time
466        # series and non-time series data. Also,
467        # it has unusually structured Excel files. So, a good test.
468
469        print("Starting test.")
470
471        d, _m = read_abs_cat("8731.0", keep_non_ts=False, verbose=False)
472        print(f"--- {len(d)=} ---")
473        print(f"--- {d.keys()=} ---")
474        for table in d:
475            freq_str = getattr(d[table].index, "freqstr", "Unknown")
476            print(f"{table=} {d[table].shape=} {freq_str=}")
477
478        print ("=" * 20)
479
480        # Optional: exercise the local zip_file path. Requires a developer to
481        # have a pre-downloaded ABS zip at this location; skipped if absent.
482        local_zip = Path(".test-data/Qrtly-CPI-Time-series-spreadsheets-all.zip")
483        if local_zip.exists():
484            d, _m = read_abs_cat("", zip_file=str(local_zip), verbose=False)
485            print(f"--- {len(d)=} ---")
486            print(f"--- {d.keys()=} ---")
487            for table in d:
488                freq_str = getattr(d[table].index, "freqstr", "Unknown")
489                print(f"{table=} {d[table].shape=} {freq_str=}")
490        else:
491            print(f"Skipping local zip_file test: {local_zip} not present.")
492
493        print("Test complete.")
494
495    simple_test()
MAX_DATETIME_CHARS = 20
TABLE_DESC_ROW = 4
TABLE_DESC_COL = 1
@cache
def read_abs_cat( cat: str, **kwargs: Unpack[readabs.ReadArgs]) -> tuple[dict[str, pandas.DataFrame], pandas.DataFrame]:
 28@cache  # minimise slowness for any repeat business
 29def read_abs_cat(
 30    cat: str,
 31    **kwargs: Unpack[ReadArgs],
 32) -> tuple[dict[str, DataFrame], DataFrame]:
 33    """For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames.
 34
 35    This function returns the complete ABS Catalogue information as a
 36    python dictionary of pandas DataFrames, as well as the associated metadata
 37    in a separate DataFrame. The function automates the collection of zip and
 38    excel files from the ABS website. If necessary, these files are downloaded,
 39    and saved into a cache directory. The files are then parsed to extract time
 40    series data, and the associated metadata.
 41
 42    By default, the cache directory is `./.readabs_cache/`. You can change the
 43    default directory name by setting the shell environment variable
 44    `READABS_CACHE_DIR` with the name of the preferred directory.
 45
 46    Parameters
 47    ----------
 48    cat : str
 49        The ABS Catalogue Number for the data to be downloaded and made
 50        available by this function. This argument must be specified in the
 51        function call.
 52
 53    **kwargs : Unpack[ReadArgs]
 54        The following parameters may be passed as optional keyword arguments.
 55
 56    url : str = ""
 57        The URL of an ABS landing page. Use this for discontinued series
 58        that are no longer in the ABS Time Series Directory. If provided,
 59        data will be retrieved from this URL instead of looking up the
 60        catalogue number. Example:
 61        `read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")`
 62
 63    keep_non_ts : bool = False
 64        A flag for whether to keep the non-time-series tables
 65        that might form part of an ABS catalogue item. Normally, the
 66        non-time-series information is ignored, and not made available to
 67        the user.
 68
 69    history : str = ""
 70        Provide a month-year string to extract historical ABS data.
 71        For example, you can set history="dec-2023" to the get the ABS data
 72        for a catalogue identifier that was originally published in respect
 73        of Q4 of 2023. Note: not all ABS data sources are structured so that
 74        this technique works in every case; but most are.
 75
 76    verbose : bool = False
 77        Setting this to true may help diagnose why something
 78        might be going wrong with the data retrieval process.
 79
 80    ignore_errors : bool = False
 81        Normally, this function will cease downloading when
 82        an error in encountered. However, sometimes the ABS website has
 83        malformed links, and changing this setting is necessitated. (Note:
 84        if you drop a message to the ABS, they will usually fix broken
 85        links with a business day).
 86
 87    get_zip : bool = True
 88        Download the excel files in .zip files.
 89
 90    get_excel_if_no_zip : bool = True
 91        Only try to download .xlsx files if there are no zip
 92        files available to be downloaded. Only downloading individual excel
 93        files when there are no zip files to download can speed up the
 94        download process.
 95
 96    get_excel : bool = False
 97        The default value means that excel files are not
 98        automatically download. Note: at least one of `get_zip`,
 99        `get_excel_if_no_zip`, or `get_excel` must be true. For most ABS
100        catalogue items, it is sufficient to just download the one zip
101        file. But note, some catalogue items do not have a zip file.
102        Others have quite a number of zip files.
103
104    single_excel_only : str = ""
105        If this argument is set to a table name (without the
106        .xlsx extension), only that excel file will be downloaded. If
107        set, and only a limited subset of available data is needed,
108        this can speed up download times significantly. Note: overrides
109        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`.
110
111    selected_excel : tuple[str, ...] = ()
112        If set to a tuple of table names (without the .xlsx extension),
113        only those excel files will be downloaded. Useful when several
114        specific tables are needed and downloading the full zip would
115        be wasteful. Example:
116        `selected_excel=("62020001", "62020017", "62020X28")`.
117        Must be a tuple (not a list) because `read_abs_cat` uses an
118        internal cache that requires hashable arguments. Note: overrides
119        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`
120        when at least one matching file is found.
121
122    single_zip_only : str = ""
123        If this argument is set to a zip file name (without
124        the .zip extension), only that zip file will be downloaded.
125        If set, and only a limited subset of available data is needed,
126        this can speed up download times significantly. Note: overrides
127        `get_zip`, `get_excel_if_no_zip`, and `get_excel`.
128
129    cache_only : bool = False
130        If set to True, this function will only access
131        data that has been previously cached. Normally, the function
132        checks the date of the cache data against the date of the data
133        on the ABS website, before deciding whether the ABS has fresher
134        data that needs to be downloaded to the cache.
135
136    zip_file: str | Path = ""
137        If set to a specific zip file name (with or without the .zip
138        extension), this function will only extract data from that zip file
139        on the local file system. This may be useful for debugging purposes.
140
141    Returns
142    -------
143    tuple[dict[str, DataFrame], DataFrame]
144        The function returns a tuple of two items. The first item is a
145        python dictionary of pandas DataFrames (which is the primary data
146        associated with the ABS catalogue item). The second item is a
147        DataFrame of ABS metadata for the ABS collection.
148
149        Note:
150        You can retrieve non-timeseries data using the grab_abs_url()
151        function. That takes the URL for the ABS landing page for the ABS
152        collection you are interested in. The read_abs_cat function is for
153        ABS catalogue identifiers which are timeseries data, for which the
154        metadata can be extracted.
155
156    Example
157    -------
158
159    ```python
160    import readabs as ra
161    from pandas import DataFrame
162    cat_num = "6202.0"  # The ABS labour force survey
163    data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num)
164    abs_dict, meta = data
165    ```
166
167    """
168    # --- get the time series data ---
169    zip_file = kwargs.get("zip_file")
170    raw_abs_dict = (
171        grab_abs_zip(zip_file, **kwargs) if zip_file else grab_abs_url(cat=cat, **kwargs)
172    )
173    response = _get_time_series_data(cat, raw_abs_dict, **kwargs)
174
175    if not response:
176        response = {}, DataFrame()
177
178    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