{"cells":[{"metadata":{},"cell_type":"markdown","source":"# What to do"},{"metadata":{},"cell_type":"markdown","source":"In [competition page](https://www.kaggle.com/c/indoor-location-navigation/data), following data quality problem is described.\n\n>A note on data quality: In the training files, you may find occasionally that a line is missing the ending newline character, causing it to run on to the next line. It is up to you how you want to handle this issue. This issue is not found in the test data.\n\nIn this note book, I will see some path files for understanding file format and try to arrange data into handy format with addressing the data quality problem above."},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import glob\nimport os\nimport os.path\nimport random\nfrom typing import Tuple, List, Union\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 1st sample"},{"metadata":{},"cell_type":"markdown","source":"First sample is \"/kaggle/input/indoor-location-navigation/train/5cd56c0ce2acfd2d33b6ab27/B1/5d09a625bd54340008acddb9.txt\". It tells me the basic format of path file."},{"metadata":{"trusted":true,"_kg_hide-output":false,"_kg_hide-input":false},"cell_type":"code","source":"sample_filepath = \"/kaggle/input/indoor-location-navigation/train/5cd56c0ce2acfd2d33b6ab27/B1/5d09a625bd54340008acddb9.txt\"\nwith open(sample_filepath, 'r') as f:\n    sample_content = f.readlines()\n    f.close()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"collapsed":true},"cell_type":"code","source":"sample_content[:20]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"collapsed":true},"cell_type":"code","source":"sample_content[-20:]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"It seems that;\n- Top 7 rows are header and final row is footer.\n  - Each row contains single or several fields separated by tab.\n  - The field format is _name + \":\" + value_\n- Other rows conatin sensor data, format is uniformed."},{"metadata":{},"cell_type":"markdown","source":"Following by [Official guide](https://github.com/location-competition/indoor-location-competition-20), each row have 4-10 columns; first is Unix time(millisecond), second is data type, the others body. We can obtain ground truth (x, y) labels from 3 and 4 columns of data type == \"TYPE_WAYPOINT\"."},{"metadata":{},"cell_type":"markdown","source":"# 2nd sample"},{"metadata":{},"cell_type":"markdown","source":"Next, try \"/kaggle/input/indoor-location-navigation/train/5da138b74db8ce0c98bd4774/F3/5db299ab5741f4000680a7d3.txt\" in order to understand the format more."},{"metadata":{"trusted":true,"_kg_hide-output":false,"_kg_hide-input":false,"collapsed":true},"cell_type":"code","source":"sample_filepath2 = \"/kaggle/input/indoor-location-navigation/train/5da138b74db8ce0c98bd4774/F3/5db299ab5741f4000680a7d3.txt\"\nwith open(sample_filepath2, 'r') as f:\n    sample_content2 = f.readlines()\n    f.close()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"collapsed":true},"cell_type":"code","source":"sample_content2[:20]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Only 2 header rows. It indicates that number of header rows is not fixed. Probably \"#\" indicates that the line is header/footer."},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"sample_content2[-20:]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 3rd sample"},{"metadata":{},"cell_type":"markdown","source":"Third one is \"/kaggle/input/indoor-location-navigation/train/5cd56b83e2acfd2d33b5cab0/B2/5cf72539e9d9c9000852f45b.txt\"."},{"metadata":{"trusted":true,"_kg_hide-output":false,"_kg_hide-input":true},"cell_type":"code","source":"sample_filepath3 =  \"/kaggle/input/indoor-location-navigation/train/5cd56b83e2acfd2d33b5cab0/B2/5cf72539e9d9c9000852f45b.txt\"\nwith open(sample_filepath3, 'r') as f:\n    sample_content3 = f.readlines()\n    f.close()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"collapsed":true},"cell_type":"code","source":"sample_content3[:20]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"collapsed":true},"cell_type":"code","source":"sample_content3[-20:]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Then I can notice that some files dose not have footer."},{"metadata":{},"cell_type":"markdown","source":"# 4th sample"},{"metadata":{},"cell_type":"markdown","source":"Check \"/kaggle/input/indoor-location-navigation/train/5cd56b90e2acfd2d33b5e33f/F1/5d0868bdbb84450008f569ca.txt\". This file shows the example of data quality problem"},{"metadata":{"trusted":true,"_kg_hide-output":false,"_kg_hide-input":true},"cell_type":"code","source":"sample_filepath4 = \"/kaggle/input/indoor-location-navigation/train/5cd56b90e2acfd2d33b5e33f/F1/5d0868bdbb84450008f569ca.txt\"\nwith open(sample_filepath4, 'r') as f:\n    sample_content4 = f.readlines()\n    f.close()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"sample_content4[:20]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"sample_content4[-20:]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Now show the data quality problem."},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"# show line 482, 483, 484, please watch 483 carefully!\nsample_content4[482:485]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Line 483 is very long, this is the data quality problem, missing \"\\n\" at the end of line. Same problem happens 56 lines in this file."},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"# Which rows have >10 rows?\nsensor_data4 = sample_content4[7:-1]\nindice = []\nfor i, line in enumerate(sensor_data4):\n    fields = [field for field in line.strip().split('\\t')]\n    if len(fields) > 10:\n        indice.append(str(i))\nprint(f'{len(indice)} lines contain > 2 records')\nprint(f'Lines problem occurd at: {\", \".join(indice)}')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"sensor_data4[994]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"sensor_data4[1497]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 5th sample"},{"metadata":{},"cell_type":"markdown","source":"Now take a look at \"/kaggle/input/indoor-location-navigation/test/52ad8c760ff9978d0949deed.txt\". It shows that sometimes header format is inconsistent."},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_filepath5 = \"/kaggle/input/indoor-location-navigation/test/52ad8c760ff9978d0949deed.txt\"\nwith open(sample_filepath5, 'r') as f:\n    sample_content5 = f.readlines()\n    f.close()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_content5[0]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Field name and value is separated not by \":\", but by \"\\t\"."},{"metadata":{},"cell_type":"markdown","source":"# Brief summary"},{"metadata":{},"cell_type":"markdown","source":"Then I should keep in my mind abount path files' charactaristics and quality;\n- Data quality problem described at competition page.\n- Number of footer row is not fixed.  \n- A file may have no footer.\n- About header format, field name and value is not always separated by \":\", sometimes by \"\\t\".\n"},{"metadata":{},"cell_type":"markdown","source":"# Convert path file addressing the data quality problem"},{"metadata":{},"cell_type":"markdown","source":"Now, I would like to try making path files more handy.\n\n- Separate .txt path files into 3 files: header, footer, body files. \n  - Convert header and footer data into simple .json file, and\n  - convert body data into .parquet file.\n- Address data quality problem.\n\nI write the solution code on next cell."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"DATA_TYPES = ('TYPE_ACCELEROMETER',\n              'TYPE_MAGNETIC_FIELD',\n              'TYPE_GYROSCOPE',\n              'TYPE_ROTATION_VECTOR',\n              'TYPE_MAGNETIC_FIELD_UNCALIBRATED',\n              'TYPE_GYROSCOPE_UNCALIBRATED',\n              'TYPE_ACCELEROMETER_UNCALIBRATED',\n              'TYPE_WIFI',\n              'TYPE_BEACON',\n              'TYPE_WAYPOINT')\n\n\n\n\ndef count_header_row(content: List[str]) -> int:\n    return min([i for i in range(len(content)) if not content[i].startswith('#')])\n\n\ndef separate_line_if_needed(line: str) -> list:\n    '''Separate multiple data rows in a single file line.\n\n    In path file, each single line expects to represent each single data row.\n    Sometimes, however, a line dose not have the line feed at the end of itself,\n    thus multiple data rows are belong to the line. This function will address\n    such data quality problem.\n    The function detects the problem by rough method. If line has greater than \n    10 columns, detect and try to address the problem. If not, treat it that \n    there are no data quality problem.\n\n    Parameters\n    ----------\n    line: str\n        Single line of sensor data file. It sometimes represents single data row,\n        sometimes multiple.\n\n    Return\n    ------\n    list of line(s): list\n        If there are multiple data rows in line, separate it so that each line\n        represents single data row. If not, return [list].\n        \n    Note\n    ----\n    This solution depends on that 1st column (Unix time) and 2nd column (data type)\n    dose not have any quality problem such as missing value, invalid value,\n    too long/short length, and so on.\n    '''\n\n    max_columns = 10\n    field_sep = '\\t'\n    fields = [field for field in line.strip().split(field_sep)]\n    idx_data_type = [i for i, field in enumerate(fields) if field in DATA_TYPES]\n    if len(fields) <= max_columns:\n        # No problem! line represents single record.\n        return [line]\n\n    \"\"\"\n    1. Identify where 1st row actually ends. The hint is where 2nd row's data type starts.\n    2. Separate line into 2 parts; \"1st row\" and \"Others\".\n    3. Add \"1st row\" of 2. to list of lines.\n    4. Separate \"Others\" into single line(s) by same process recursively.\n    \"\"\"\n    lines = []\n    len_unix_time = 13  # Unix time milliseconds, such as '1560830841553'\n    \n    # 1.\n    # where 2nd row's data type starts ?\n    idx_second_data_type = idx_data_type[1]\n    almost_first_row = field_sep.join(fields[:idx_second_data_type])  # 1st row + 2nd row's first column\n    pos_first_row_end = len(almost_first_row) - len_unix_time  # Where 1st row actually ends\n    # 2.\n    first_row = line[:pos_first_row_end]\n    others = line[pos_first_row_end:]\n    # 3.\n    lines.append(first_row)\n    # 4.\n    for l in others.splitlines():\n        lines += separate_line_if_needed(l)\n\n    return lines\n\n\ndef to_dict(header_or_footer: Union[str, list]) -> Union[None, dict]:\n    '''Convert header/footer string into dictionary object.\n    \n    In path file, format of header and footer is described as follows;\n    - header and footer line is starts with fixed string '#\\t'\n    - header and footer is composed of combinations of field name and value.\n      i.e. '#\\tSiteID:5cd56b83e2acfd2d33b5cab0\\tSiteName:日月光中心'\n    Field name and value is separated by generally ':'. Sometimes, however,\n    they are separated by '\\t'. This function addresses such inconsistent\n    format problem partially.\n    \n    Parameters\n    ----------\n    header_or_footer: str or list\n        header or footer represented by string, or list of them.\n        \n    Return\n    ------\n    dctionary or None:\n        Return None if header_or_footer is actually a header/footer,\n        otherwise dictionary (key: field name, value: field value).\n    '''\n    \n    result_dict = {}\n    if isinstance(header_or_footer, str):\n        header_or_footer = [header_or_footer]\n    \n    header_or_footer = [l for l in header_or_footer if l.startswith('#\\t')]\n    if not header_or_footer:\n        return None\n\n    for line in header_or_footer:\n        # We do not need First 3 chars '#\\t\" and line feed.\n        fields = line[2:].strip().split('\\t')\n        skip = False\n        for i, field in enumerate(fields):\n            if skip:\n                skip = False\n                continue\n            try:\n                name, value = field.split(':')\n            except ValueError:\n                # Field name and value might be separated by \"\\t\"\n                name, value = fields[i], fields[i + 1]\n                skip = True\n            result_dict[name] = value\n    return result_dict","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"_kg_hide-input":true,"collapsed":true},"cell_type":"code","source":"assert(count_header_row(sample_content) == 7)\nsample_content[:10]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"_kg_hide-input":true,"collapsed":true},"cell_type":"code","source":"assert(count_header_row(sample_content2) == 2)\nsample_content2[:10]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true,"_kg_hide-input":true,"collapsed":true},"cell_type":"code","source":"assert(count_header_row(sample_content3) == 7)\nsample_content3[:10]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"single = '1560830842594\\tTYPE_BEACON\\t3rd_column\\t4th_column\\t5th_column\\t6th_column\\t7th_column\\t8th_column\\t9th_column\\t10th_column'\nassert(separate_line_if_needed(single) == [single])\n\nsingle2 = '1560830842594\\tTYPE_WAYPOINT\\t3rd_column\\t4th_column'\nassert(separate_line_if_needed(single2) == [single2])\n\nnot_single = single + single2\nassert(separate_line_if_needed(not_single) == [single, single2])\n\nnot_single2 = single + single2 + single + single2\nassert(separate_line_if_needed(not_single2) == [single, single2, single, single2])\n\nvery_long = ''.join([single] * 100)\nassert(separate_line_if_needed(very_long) == [single] * 100)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"header1 = (\n    '#\\tstartTime:1559699278686\\n',\n    '#\\tSiteID:5cd56b83e2acfd2d33b5cab0\\tSiteName:日月光中心\\tFloorId:5cd56b86e2acfd2d33b5d1f6\\tFloorName:B2\\n')\nassert(to_dict(header1) == {'startTime': '1559699278686',\n                            'SiteID': '5cd56b83e2acfd2d33b5cab0',\n                            'SiteName': '日月光中心',\n                            'FloorId':'5cd56b86e2acfd2d33b5d1f6',\n                            'FloorName': 'B2'})\nheader2 = (\n    '#\\tstartTime\\t1559699278686\\n',\n    '#\\tSiteID:5cd56b83e2acfd2d33b5cab0\\tSiteName:日月光中心\\tFloorId:5cd56b86e2acfd2d33b5d1f6\\tFloorName:B2\\n')\nassert(to_dict(header2) == {'startTime': '1559699278686',\n                            'SiteID': '5cd56b83e2acfd2d33b5cab0',\n                            'SiteName': '日月光中心',\n                            'FloorId':'5cd56b86e2acfd2d33b5d1f6',\n                            'FloorName': 'B2'})\n\nheader3 = (\n    '#\\tstartTime:1559699278686\\n',\n    '#\\tSiteID:5cd56b83e2acfd2d33b5cab0\\tSiteName\\t日月光中心\\tFloorId:5cd56b86e2acfd2d33b5d1f6\\tFloorName:B2\\n')\nassert(to_dict(header3) == {'startTime': '1559699278686',\n                            'SiteID': '5cd56b83e2acfd2d33b5cab0',\n                            'SiteName': '日月光中心',\n                            'FloorId':'5cd56b86e2acfd2d33b5d1f6',\n                            'FloorName': 'B2'})\n\nfooter1 = '#\\tendTime\\t0000000201304\\n'\nassert(to_dict(footer1) == {'endTime': '0000000201304'})\nfooter2 = '#\\tendTime:0000000201304\\n'\nassert(to_dict(footer2) == {'endTime': '0000000201304'})\nfooter3 = '1559699299331\\tTYPE_ROTATION_VECTOR\\t0.17401376\\t-0.0031055238'  # Not a footer\nassert(to_dict(footer3) is None)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Maybe no problem.\n\nNow convert path file data into more handy format; header and footer into json file, path data into parquet file."},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nimport json\nfrom time import time\nfrom logging import getLogger, INFO, WARNING, FileHandler, StreamHandler, Formatter\n\n# preparation for logging\nlogger = getLogger(__name__)\nlogger.setLevel(INFO)\nformatter = Formatter('%(levelname)s : %(asctime)s : %(message)s')\nfilehandler = FileHandler('notebook.log')\nfilehandler.setLevel(INFO)\nfilehandler.setFormatter(formatter)\nlogger.addHandler(filehandler)\nstreamhandler = StreamHandler()\nstreamhandler.setLevel(WARNING)\nstreamhandler.setFormatter(formatter)\nlogger.addHandler(streamhandler)\n\nlogger.info(f'Start!')\n\ntrain_filepaths = glob.glob(\"/kaggle/input/indoor-location-navigation/train/*/*/*.txt\", recursive=True)\ntest_filepaths = glob.glob(\"/kaggle/input/indoor-location-navigation/test/*.txt\", recursive=True)\nfilepaths = train_filepaths + test_filepaths\n\nn_files = len(filepaths)\nlogger.info('{} path files found'.format(n_files))\nsince = time()\n\nfor i, filepath in enumerate(filepaths):\n    logger.info('Start processing \"{}\"'.format(filepath))\n\n    with open(filepath, 'r') as f:\n        content = f.readlines()\n        f.close()\n\n    # arrange footer into dictionary object\n    footer = content[-1]\n    arranged_footer = to_dict(footer)  \n\n    # arrange header into dictionary object\n    n_header_rows = count_header_row(content)\n    header = content[:n_header_rows]\n    arranged_header = to_dict(header)\n    \n    # arrange body into pd.DataFrame object\n    body = content[n_header_rows:-1]\n    len_columns = 10\n    columns = [f'column{i + 1}' for i in range(len_columns)]\n    data = []\n    for line in body:\n        cleansed_line = separate_line_if_needed(line)  # data cleansing\n        for line_ in cleansed_line:\n            fields = [field for field in line_.strip().split('\\t')]\n            if len(fields) < len_columns:\n                # padding\n                fields += [np.nan] * (len_columns - len(fields))\n            data.append(fields)\n    arranged_body = pd.DataFrame(data=data, columns=columns)\n    \n    # prepare directory to save dataset\n    train_or_test = filepath.split(os.path.sep)[-4]\n    if train_or_test == 'train':\n        site = filepath.split(os.path.sep)[-3]\n        floor = filepath.split(os.path.sep)[-2]\n        body_directory = os.path.join(train_or_test, site, floor)\n    else:\n        body_directory = 'test'\n    header_directory = os.path.join(body_directory, 'header')\n    footer_directory = os.path.join(body_directory, 'footer')\n    for directory in (body_directory, header_directory, footer_directory):\n        os.makedirs(directory, exist_ok=True)\n    \n    # save dataset:\n    ## footer\n    basename = filepath.split(os.path.sep)[-1][:-4]\n    footer_filepath = os.path.join(footer_directory, basename + '.json')\n    if arranged_footer is not None:\n        with open(footer_filepath, 'w') as f:\n            json.dump(arranged_footer, f, indent=4)\n            f.close()\n    else:\n        pass\n    ## header\n    header_filepath = os.path.join(header_directory, basename + '.json')\n    with open(header_filepath, 'w') as f:\n        json.dump(arranged_header, f, indent=4)\n        f.close()\n    ## body\n    body_filepath = os.path.join(body_directory, basename + '.parquet')\n    arranged_body.to_parquet(body_filepath, index=False)\n\n    # logging\n    if arranged_footer is None:\n        logger.warning('Separate \"{}\" into 2 files: header=\"{}\", body=\"{}\", no footer file' \\\n                    .format(filepath, header_filepath, body_filepath))\n    else:\n        logger.info('Separate \"{}\" into 3 files: header=\"{}\", footer=\"{}\", body=\"{}\"' \\\n                    .format(filepath, header_filepath, footer_filepath, body_filepath))\n    logger.info('{}/{} files has processed ({} seconds passed)' \\\n                .format(i + 1, n_files, time() - since))\n\nlogger.info('Complete!')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"headers = []\nfor header_filepath in glob.glob('train/*/*/header/*.json'):\n    with open(header_filepath, 'r') as f:\n        headers.append(json.load(f))\n        f.close()\npd.DataFrame(headers)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"headers = []\nfor header_filepath in glob.glob('test/header/*.json'):\n    with open(header_filepath, 'r') as f:\n        headers.append(json.load(f))\n        f.close()\npd.DataFrame(headers)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"footers = []\nfor footer_filepath in glob.glob('train/*/*/footer/*.json'):\n    with open(footer_filepath, 'r') as f:\n        footers.append(json.load(f))\n        f.close()\npd.DataFrame(footers)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"footers = []\nfor footer_filepath in glob.glob('test/footer/*.json'):\n    with open(footer_filepath, 'r') as f:\n        footers.append(json.load(f))\n        f.close()\npd.DataFrame(footers)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"%%time\n!zip -rm \"train.zip\" \"train\"","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"%%time\n!zip -rm \"test.zip\" \"test\"","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"print('Complete!')","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}