diff --git a/dbdpy/dbdpy_v1.ipynb b/dbdpy/dbdpy_v1.ipynb new file mode 100644 index 00000000..21e8a24b --- /dev/null +++ b/dbdpy/dbdpy_v1.ipynb @@ -0,0 +1,625 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "import glob\n", + "import seaborn as sns\n", + "import unittest\n", + "from unittest import TestCase\n", + "import json\n", + "\n", + "# For testing in an ipynb:\n", + "# import importlib\n", + "# importlib.reload(FitBit)\n", + "# Run second line in a cell everytime you make changes to FitBit.py" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "class FitBit:\n", + " def __init__(self, file_path):\n", + " \"\"\"\n", + " Initialize FitBit object and load all data.\n", + " \n", + " Parameters\n", + " ----------\n", + " file_path : str\n", + " The path to the folder containing all data collected from FitBit.\n", + " \n", + " Returns\n", + " -------\n", + " FitBit\n", + " An instance of the FitBit class with various DataFrames loaded.\n", + " \"\"\"\n", + "\n", + " # Initialize file_path attribute\n", + " self.file_path = file_path\n", + "\n", + " # Load various types of data into respective attributes\n", + " # Using load_and_concat function to fetch and concatenate data files matching the pattern\n", + " # Each attribute will hold a DataFrame containing the respective type of data\n", + "\n", + " #self.sleep = self.load_and_concat('/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/sleep-*.json')\n", + " #self.energy = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/calories-*.json\")\n", + " #self.steps = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/steps-*.json\")\n", + " #self.distance = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/distance-*.json\")\n", + " self.oxygen = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/estimated_oxygen_variation-*.json\")\n", + " self.resting_heart_rate = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/resting_heart_rate-*.json\")\n", + " #self.heart_rate = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/heart_rate-*.json\")\n", + " self.respiration_rate = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/distance-*.json\")\n", + " #self.sleep_stage = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/sleep-*.json\")\n", + " self.floors_climbed = self.load_and_concat(\"/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data/Global Export Data/altitude-*.json\")\n", + " \n", + " \n", + " def load(self, file_path):\n", + " \"\"\"\n", + " Load data from a specified file path.\n", + "\n", + " Parameters\n", + " ----------\n", + " file_path : str\n", + " The path to the data file.\n", + " \n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " The loaded data as a pandas DataFrame.\n", + " \"\"\"\n", + "\n", + " # Update the file path\n", + " self.file_path = file_path\n", + "\n", + " # Check if the file exists\n", + " if os.path.exists(self.file_path):\n", + "\n", + " # Get file format (csv or json)\n", + " file = self.file_path.split('/')[-1]\n", + " file_name, file_format = file.split('.')\n", + "\n", + " # Load data depending on file format\n", + " if file_format == 'csv':\n", + " print(f\"CSV data loaded from {self.file_path}\")\n", + " return pd.read_csv(self.file_path)\n", + " elif file_format == 'json':\n", + " print(f\"JSON data loaded from {self.file_path}\")\n", + " return pd.read_json(self.file_path)\n", + " else:\n", + " print(f\"Unsupported file format: {file_format}\")\n", + " else:\n", + " print(f\"The path {self.file_path} does not exist.\")\n", + " \n", + " def load_and_concat(self, pattern):\n", + " \"\"\"\n", + " Load and concatenate multiple files that match the given file name pattern.\n", + " \n", + " Parameters\n", + " ----------\n", + " pattern : str\n", + " The file name pattern to search for.\n", + " \n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " A DataFrame consisting of concatenated data from all matched files.\n", + " Returns an empty DataFrame if no data was found.\n", + " \"\"\"\n", + "\n", + " # Get list of all file paths that match the pattern\n", + " file_paths = glob.glob(os.path.join(self.file_path, pattern))\n", + " data_frames = []\n", + "\n", + " # Loop through all matched file paths\n", + " for file_path in file_paths:\n", + " # Load each file into a DataFrame using the load method\n", + " df = self.load(file_path)\n", + " # Append DataFrame to the list if it's not None\n", + " if df is not None:\n", + " data_frames.append(df)\n", + "\n", + " # Concatenate all DataFrames if the list is not empty\n", + " if data_frames:\n", + " return pd.concat(data_frames, ignore_index=True)\n", + " else:\n", + " # Return an empty DataFrame if no data was found\n", + " return pd.DataFrame()\n", + " \n", + " def heart_rate(self,confidence=True):\n", + " '''Get heart_rate dataframe \n", + " \n", + " Returns\n", + " -------\n", + " heart_rate: pandas.DataFrame\n", + " Dataframe with columns for timestamp (datetime64) and heart_rate in bpm (int)\n", + " '''\n", + " temp_df = self.load_and_concat(\"/workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-*.json\")\n", + " df_unpacked = pd.concat([temp_df.drop('value', axis=1), pd.json_normalize(temp_df['value'])], axis=1)\n", + " df_unpacked = df_unpacked.sort_values(by='dateTime') \n", + " df_unpacked = df_unpacked.reset_index(drop=True)\n", + " if confidence: \n", + " df = df_unpacked[['dateTime','bpm','confidence']]\n", + " else:\n", + " df = df_unpacked[['dateTime','bpm']]\n", + " return df\n", + "\n", + " ### Still need to figure out the unit and also why the number decreases as time goes on?\n", + " def active_calories(self):\n", + " '''Get active_calories dataframe \n", + " \n", + " Returns\n", + " -------\n", + " active_calories: pandas.DataFrame\n", + " Dataframe with columns for start_time (datetime64), end_time (datetime64) and active calories in kilocalories (int)\n", + " '''\n", + " temp_df = self.load_and_concat(\"/workspaces/fitbit_dbdp/data/Global Export Data/calories-*.json\")\n", + " # Add datetime of the next row\n", + " temp_df['end_time'] = temp_df['dateTime'].shift(-1)\n", + " temp_df['next_value'] = temp_df['value'].shift(-1)\n", + " temp_df['active calories'] = temp_df['next_value'] - temp_df['value']\n", + " df = temp_df[(temp_df['active calories']>0.00)&(~temp_df['active calories'].isna())]\n", + " df=df[['dateTime','end_time','active calories']]\n", + " return df\n", + "\n", + "\n", + " def steps(self):\n", + " '''Get steps dataframe \n", + " \n", + " Returns\n", + " -------\n", + " steps: pandas.DataFrame\n", + " Dataframe with columns for start_time (datetime64), end_time (datetime64) and step count (int)\n", + " '''\n", + " temp_df = self.load_and_concat(\"/workspaces/fitbit_dbdp/data/Global Export Data/steps-*.json\")\n", + " temp_df['end_time'] = temp_df['dateTime'].shift(-1)\n", + " temp_df['next_value'] = temp_df['value'].shift(-1)\n", + " temp_df['steps'] = temp_df['next_value'] + temp_df['value']\n", + " df=temp_df[(temp_df['steps']>0.00)&(~temp_df['steps'].isna())]\n", + " df=df[['dateTime','end_time','steps']]\n", + " return df\n", + " \n", + "\n", + " def sleep_stage_summary(self):\n", + " '''Get summary of sleep stages for the night of a day\n", + " \n", + " Returns\n", + " -------\n", + " sleep_stage_summary: pandas.DataFrame\n", + " Dataframe with columns for date (datetime64), wake (seconds, int), light (seconds, int), deep (seconds, itn), REM (seconds, int) '''\n", + " \n", + " temp_df = self.load_and_concat(\"/workspaces/fitbit_dbdp/data/Global Export Data/sleep-*.json\")[['dateOfSleep','levels']]\n", + " temp_df['light'] = temp_df['levels'].apply(lambda x: x.get('summary', {}).get('light', {}).get('minutes'))\n", + " temp_df['deep'] = temp_df['levels'].apply(lambda x: x.get('summary', {}).get('deep', {}).get('minutes'))\n", + " temp_df['REM'] = temp_df['levels'].apply(lambda x: x.get('summary', {}).get('rem', {}).get('minutes'))\n", + " temp_df['wake'] = temp_df['levels'].apply(lambda x: x.get('summary', {}).get('wake', {}).get('minutes'))\n", + " df = temp_df[['dateOfSleep','light','deep','REM','wake']]\n", + " return df\n", + " \n", + " def distance(self):\n", + " '''Get distance dataframe \n", + " \n", + " Returns\n", + " -------\n", + " distance: pandas.DataFrame\n", + " Dataframe with columns for start_time (datetime64), end_time (datetime64) and distance in meters (int)\n", + " '''\n", + " temp_df = temp_df = self.load_and_concat(\"/workspaces/fitbit_dbdp/data/Global Export Data/distance-*.json\")\n", + " return temp_df\n", + " \n", + " def spo2(self):\n", + " '''Get SpO2 dataframe \n", + " \n", + " Returns\n", + " -------\n", + " spo2: pandas.DataFrame\n", + " Dataframe with columns for timestamp (datetime64) and SpO2 % (int)\n", + " '''\n", + " return self._spo2()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-22.json\n", + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-26.json\n", + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-20.json\n", + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-23.json\n", + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-25.json\n", + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-24.json\n", + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/heart_rate-2023-09-21.json\n", + " dateTime bpm confidence\n", + "0 2023-09-20 19:33:32 70 0\n", + "1 2023-09-20 19:33:42 75 0\n", + "2 2023-09-20 19:33:47 108 1\n", + "3 2023-09-20 19:33:52 101 1\n", + "4 2023-09-20 19:33:57 92 1\n", + "... ... ... ...\n", + "72032 2023-09-27 03:58:43 69 1\n", + "72033 2023-09-27 03:58:53 67 1\n", + "72034 2023-09-27 03:58:58 68 1\n", + "72035 2023-09-27 03:59:08 69 1\n", + "72036 2023-09-27 03:59:23 69 1\n", + "\n", + "[72037 rows x 3 columns]\n" + ] + } + ], + "source": [ + "file_path = '/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data'\n", + "fitbit= FitBit(file_path)\n", + "heart_rate = fitbit.heart_rate()\n", + "print(heart_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/calories-2023-09-19.json\n", + " dateTime end_time active calories\n", + "2371 2023-09-20 15:31:00 2023-09-20 15:32:00 0.59\n", + "2373 2023-09-20 15:33:00 2023-09-20 15:34:00 1.99\n", + "2375 2023-09-20 15:35:00 2023-09-20 15:36:00 2.34\n", + "2376 2023-09-20 15:36:00 2023-09-20 15:37:00 2.35\n", + "2380 2023-09-20 15:40:00 2023-09-20 15:41:00 0.23\n", + "... ... ... ...\n", + "12414 2023-09-27 14:54:00 2023-09-27 14:55:00 0.70\n", + "12415 2023-09-27 14:55:00 2023-09-27 14:56:00 0.71\n", + "12416 2023-09-27 14:56:00 2023-09-27 14:57:00 3.74\n", + "12418 2023-09-27 14:58:00 2023-09-27 14:59:00 0.47\n", + "12419 2023-09-27 14:59:00 2023-09-27 15:00:00 0.47\n", + "\n", + "[2555 rows x 3 columns]\n" + ] + } + ], + "source": [ + "file_path = '/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data'\n", + "fitbit= FitBit(file_path)\n", + "energy = fitbit.active_calories()\n", + "print(energy)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/steps-2023-09-19.json\n", + " dateTime end_time steps\n", + "1 2023-09-20 19:33:00 2023-09-20 19:34:00 11.0\n", + "2 2023-09-20 19:34:00 2023-09-20 19:35:00 17.0\n", + "3 2023-09-20 19:35:00 2023-09-20 19:36:00 71.0\n", + "4 2023-09-20 19:36:00 2023-09-20 19:37:00 146.0\n", + "5 2023-09-20 19:37:00 2023-09-20 19:38:00 107.0\n", + "... ... ... ...\n", + "5588 2023-09-27 19:00:00 2023-09-27 19:01:00 216.0\n", + "5589 2023-09-27 19:01:00 2023-09-27 19:02:00 216.0\n", + "5590 2023-09-27 19:02:00 2023-09-27 19:03:00 108.0\n", + "5591 2023-09-27 19:03:00 2023-09-27 19:04:00 39.0\n", + "5592 2023-09-27 19:04:00 2023-09-27 19:05:00 37.0\n", + "\n", + "[2493 rows x 3 columns]\n" + ] + } + ], + "source": [ + "file_path = '/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data'\n", + "fitbit= FitBit(file_path)\n", + "step = fitbit.steps()\n", + "print(step)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/sleep-2023-09-19.json\n", + " dateOfSleep light deep REM wake\n", + "0 2023-09-27 277.0 55.0 29.0 69.0\n", + "1 2023-09-26 197.0 106.0 71.0 69.0\n", + "2 2023-09-25 NaN NaN NaN NaN\n", + "3 2023-09-24 208.0 120.0 94.0 46.0\n", + "4 2023-09-23 266.0 58.0 98.0 42.0\n", + "5 2023-09-22 240.0 59.0 53.0 44.0\n", + "6 2023-09-21 247.0 42.0 67.0 36.0\n", + "7 2023-09-20 NaN NaN NaN NaN\n" + ] + } + ], + "source": [ + "file_path = '/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data'\n", + "fitbit= FitBit(file_path)\n", + "sleep = fitbit.sleep_stage_summary()\n", + "print(sleep)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JSON data loaded from /workspaces/fitbit_dbdp/data/Global Export Data/distance-2023-09-19.json\n", + " dateTime value\n", + "0 2023-09-20 19:32:00 0\n", + "1 2023-09-20 19:33:00 0\n", + "2 2023-09-20 19:34:00 820\n", + "3 2023-09-20 19:35:00 440\n", + "4 2023-09-20 19:36:00 4850\n", + "... ... ...\n", + "5591 2023-09-27 19:03:00 140\n", + "5592 2023-09-27 19:04:00 2760\n", + "5593 2023-09-27 19:05:00 0\n", + "5594 2023-09-27 19:06:00 0\n", + "5595 2023-09-27 19:07:00 0\n", + "\n", + "[5596 rows x 2 columns]\n" + ] + } + ], + "source": [ + "file_path = '/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data'\n", + "fitbit= FitBit(file_path)\n", + "distance = fitbit.distance()\n", + "print(distance)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ">\n" + ] + } + ], + "source": [ + "#####\n", + "# Unit Test\n", + "#####\n", + "\n", + "file_path = '/Users/harrisonkane/Desktop/BME/fitbit_dbdp/data'\n", + "fitbit= FitBit(file_path)\n", + "\n", + "print(fitbit.heart_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Preprocess:\n", + " def __init__(self,data,column):\n", + " self.data = data\n", + " self.column = column\n", + "\n", + " @classmethod\n", + " def imputeData(self, method='mean'):\n", + " \"\"\"\n", + " Impute missing values in the data.\n", + " Args:\n", + " data (pd.DataFrame): Input data containing missing values.\n", + " Returns:\n", + " pd.DataFrame: Data with missing values imputed.\n", + " \"\"\"\n", + "\n", + " if method == 'mean':\n", + " self.data[self.column] = self.data[self.column].fillna(self.data[self.column].mean())\n", + " elif method == 'median':\n", + " self.data[self.column] = self.data[self.column].fillna(self.data[self.column].median())\n", + " elif method == 'zero':\n", + " self.data[self.column] = self.data[self.column].fillna(0)\n", + " return self.data\n", + "\n", + " @classmethod\n", + " def sampleData(self, downsample=True, sample_rate=0):\n", + " \"\"\"\n", + " Sample the data, optionally downsampling it.\n", + " Args:\n", + " data (pd.DataFrame): Input data for sampling.\n", + " downsample (bool): Whether to downsample the data.\n", + " Returns:\n", + " pd.DataFrame: Sampled data.\n", + " \"\"\"\n", + " \n", + " if downsample == True:\n", + " sampled_data = self.data.sample(frac=sample_rate, random_state=1)\n", + " else: \n", + " if not isinstance(self.data.index, pd.DatetimeIndex):\n", + " self.data.index = pd.to_datetime(self.data.index)\n", + " data_index = self.data.index\n", + " sampled_data = self.data.resample(rule=str(sample_rate)+'D').asfreq()\n", + " sampled_data.interpolate(method='linear', inplace=True)\n", + " sampled_data = sampled_data.reindex(data_index, method='nearest')\n", + " return sampled_data\n", + "\n", + " @classmethod\n", + " def covertTime(self, time_col, timezone='UTC'):\n", + " \"\"\"\n", + " Convert a specific column in the data to a time format.\n", + " Args:\n", + " data (pd.DataFrame): Input data containing time data.\n", + " time_col (str): Name of the time column to convert.\n", + " Returns:\n", + " pd.DataFrame: Data with the time column converted.\n", + " \"\"\"\n", + " # FOR FUTURE: with standard format, shouldn't need to ask for time_col\n", + " # As all DataFrames will have same name for time column\n", + " # Also won't need to convert to datetime because will already be converted\n", + "\n", + " # Ensure the specified column exists in the DataFrame\n", + " if time_col not in self.data.columns:\n", + " raise ValueError(f\"Column '{time_col}' not found in the DataFrame.\")\n", + "\n", + " # Convert column to DateTime format (although it already should be in DateTime format)\n", + " self.data[time_col] = pd.to_datetime(self.data[time_col], errors='coerce')\n", + "\n", + " # Apply the specified timezone (default is 'UTC')\n", + " self.data[time_col] = self.data[time_col].dt.tz_localize(timezone)\n", + "\n", + " return self.data\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#####\n", + "# Unit Test\n", + "#####\n", + "\n", + "data = pd.DataFrame({'A': [1, 2, None, 4, 5], 'B': [5, 4, 3, None, 1]})\n", + "preprocessor = Preprocess(data, 'A')\n", + "\n", + "assert preprocessor.imputeData('mean').isnull().sum().sum(), 0\n", + "assert preprocessor.imputeData('median').isnull().sum().sum(), 0\n", + "assert preprocessor.imputeData('zero').isnull().sum().sum(), 0\n", + "\n", + "# Test sampleData method for downsampling\n", + "downsampled_data = preprocessor.sampleData(downsample=True, sample_rate=0.5)\n", + "assert downsampled_data.shape[0] < data.shape[0]\n", + "\n", + "# # Test sampleData method for upsampling\n", + "upsampled_data = preprocessor.sampleData(downsample=False, sample_rate=2)\n", + "upsampled_data\n", + "# assert upsampled_data.shape[0] > data.shape[0]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class EDA:\n", + " def __init__(self,data):\n", + " ## Assume it is already processed\n", + " self.data = data\n", + " self.interval = None\n", + " self.mean = None\n", + " self.median = None\n", + " self.std = None\n", + " \n", + " @classmethod\n", + " def describeData(self):\n", + " self.mean = np.mean(self.data)\n", + " self.median = np.median(self.data)\n", + " self.interval = np.max(self.data) - np.min(self.data)\n", + " self.std = np.std(self.data)\n", + " print(f\"Mean: {self.mean:.5f}\")\n", + " print(f\"Median: {self.median:.5f}\")\n", + " print(f\"Interval: {self.interval:.5f}\")\n", + " print(f\"Standard Deviation: {self.std:.5f}\")\n", + "\n", + " @classmethod\n", + " def dataDistribution(self):\n", + " plt.figure(figsize=(8, 6))\n", + " sns.distplot(self.data, bins=30, kde=False, color='blue')\n", + " plt.title('Data Distribution Plot')\n", + " plt.xlabel('Values')\n", + " plt.ylabel('Frequency')\n", + " plt.show()\n", + " \n", + " @classmethod\n", + " def detectOutliers(self):\n", + " iqr = np.percentile(self.data, 75) - np.percentile(self.data, 25)\n", + " lower_bound = np.percentile(self.data, 25) - 1.5 * iqr\n", + " upper_bound = np.percentile(self.data, 75) + 1.5 * iqr\n", + " outliers = [x for x in self.data if x < lower_bound or x > upper_bound]\n", + " print(\"Outliers:\", outliers)\n", + " \n", + " @classmethod\n", + " def correlationAnalysis(self,other_data):\n", + " # Add code to perform correlation analysis\n", + " pass\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#####\n", + "# Unit Test\n", + "#####\n", + "\n", + "data = np.random.normal(0, 1, 1000)\n", + "eda = EDA(data)\n", + "eda.describeData()\n", + "eda.dataDistribution()\n", + "eda.detectOutliers()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.10.6 ('fitbit': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "51a67541d439c69df5363ee44e1645bc59903267799ca2fd91b2dc45053bde71" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}