{"id":3695,"date":"2025-05-09T07:02:24","date_gmt":"2025-05-09T07:02:24","guid":{"rendered":"https:\/\/mailitics.com\/index.php\/2025\/05\/09\/clustering-eating-behaviors-in-time-a-machine-learning-approach-to-preventive-health\/"},"modified":"2025-05-09T07:02:24","modified_gmt":"2025-05-09T07:02:24","slug":"clustering-eating-behaviors-in-time-a-machine-learning-approach-to-preventive-health","status":"publish","type":"post","link":"https:\/\/mailitics.com\/index.php\/2025\/05\/09\/clustering-eating-behaviors-in-time-a-machine-learning-approach-to-preventive-health\/","title":{"rendered":"Clustering Eating Behaviors in Time: A Machine Learning Approach to Preventive Health"},"content":{"rendered":"<p>    Clustering Eating Behaviors in Time: A Machine Learning Approach to Preventive Health<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n    <!-- no image --><br \/>\n \t<BR><br \/>\n<BR><\/BR><\/p>\n<div>\n<p class=\"wp-block-paragraph\" id=\"ed77\">It\u2019s well <mdspan datatext=\"el1746728528988\" class=\"mdspan-comment\">known<\/mdspan> that\u00a0<em>what<\/em>\u00a0we eat matters \u2014 but what if\u00a0<em>when<\/em>\u00a0and\u00a0<em>how often<\/em>\u00a0we eat matters just as much?<\/p>\n<p class=\"wp-block-paragraph\" id=\"60a2\">In the midst of ongoing scientific debate around the benefits of\u00a0<strong>intermittent fasting<\/strong>, this question becomes even more intriguing. As someone passionate about machine learning and healthy living, I was inspired by a 2017 research paper[1] exploring this intersection. The authors introduced a novel distance metric called\u00a0<strong>Modified Dynamic Time Warping (MDTW)<\/strong>\u00a0\u2014 a technique designed to account not only for the nutritional content of meals but also their\u00a0<strong>timing<\/strong>\u00a0throughout the day.<\/p>\n<p class=\"wp-block-paragraph\" id=\"552e\">Motivated by their work[1], I built a full implementation of MDTW from scratch using Python. I applied it to cluster simulated individuals into\u00a0<strong>temporal dietary patterns<\/strong>, uncovering distinct behaviors like skippers, snackers, and night eaters.<\/p>\n<p class=\"wp-block-paragraph\" id=\"3e8f\">While MDTW may sound like a niche metric, it fills a critical gap in time-series comparison. Traditional distance measures \u2014 such as Euclidean distance or even classical Dynamic Time Warping (DTW) \u2014 struggle when applied to dietary data. People don\u2019t eat at fixed times or with consistent frequency. They skip meals, snack irregularly, or eat late at night.<\/p>\n<p class=\"wp-block-paragraph\"><strong>MDTW is designed for exactly this kind of temporal misalignment and behavioral variability.<\/strong>\u00a0By allowing flexible alignment while penalizing mismatches in both nutrient content and meal timing, MDTW reveals subtle but meaningful differences in how people eat.<\/p>\n<h2 class=\"wp-block-heading\" id=\"5443\">What this article covers:<\/h2>\n<ol class=\"wp-block-list\">\n<li class=\"wp-block-list-item\">\n<strong>Mathematical foundation of MDTW<\/strong>\u00a0\u2014 explained intuitively.<\/li>\n<li class=\"wp-block-list-item\">\n<strong>From formula to code<\/strong>\u00a0\u2014 implementing MDTW in Python with dynamic programming.<\/li>\n<li class=\"wp-block-list-item\">\n<strong>Generating synthetic dietary data<\/strong>\u00a0to simulate real-world eating behavior.<\/li>\n<li class=\"wp-block-list-item\">\n<strong>Building a distance matrix<\/strong>\u00a0between individual eating records.<\/li>\n<li class=\"wp-block-list-item\">\n<strong>Clustering individuals<\/strong>\u00a0with K-Medoids and evaluating with silhouette and elbow methods.<\/li>\n<li class=\"wp-block-list-item\">\n<strong>Visualizing clusters<\/strong>\u00a0as scatter plots and joint distributions.<\/li>\n<li class=\"wp-block-list-item\">\n<strong>Interpreting temporal patterns<\/strong>\u00a0from clusters: who eats when and how much?<\/li>\n<\/ol>\n<h2 class=\"wp-block-heading\" id=\"91b1\">Quick Note on Classical Dynamic Time Warping (DTW)<\/h2>\n<p class=\"wp-block-paragraph\" id=\"466a\">Dynamic Time Warping (DTW) is a classic algorithm used to measure similarity between two sequences that may vary in length or timing. It\u2019s widely used in speech recognition, gesture analysis, and time series alignment. Let\u2019s see a very simple example of the Sequence A is aligned to Sequence B (shifted version of B) with using traditional dynamic time warping algorithm using\u00a0<em>fastdtw<\/em>\u00a0library. As input, we give a distance metric as Euclidean. Also, we put time series to calculate the distance between these time series and optimized aligned path.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import numpy as np\nimport matplotlib.pyplot as plt\nfrom fastdtw import fastdtw\nfrom scipy.spatial.distance import euclidean\n# Sample sequences (scalar values)\nx = np.linspace(0, 3 * np.pi, 30)\ny1 = np.sin(x)\ny2 = np.sin(x+0.5)  # Shifted version\n# Convert scalars to vectors (1D)\ny1_vectors = [[v] for v in y1]\ny2_vectors = [[v] for v in y2]\n# Use absolute distance for scalars\ndistance, path = fastdtw(y1_vectors, y2_vectors, dist=euclidean)\n#or for scalar \n# distance, path = fastdtw(y1, y2, dist=lambda x, y: np.abs(x-y))\n\ndistance, path = fastdtw(y1, y2,dist=lambda x, y: np.abs(x-y))\n# Plot the alignment\nplt.figure(figsize=(10, 4))\nplt.plot(y1, label='Sequence A (slow)')\nplt.plot(y2, label='Sequence B (shifted)')\n\n# Draw alignment lines\nfor (i, j) in path:\n    plt.plot([i, j], [y1[i], y2[j]], color='gray', linewidth=0.5)\n\nplt.title(f'Dynamic Time Warping Alignment (Distance = {distance:.2f})')\nplt.xlabel('Time Index')\nplt.legend()\nplt.tight_layout()\nplt.savefig('dtw_alignment.png')\nplt.show()\n<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-77.png?ssl=1\" alt=\"\" class=\"wp-image-603582\"><figcaption class=\"wp-element-caption\">Illustration of the application of dynamic time warping to two time series (Image by author)<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\">The path returned by <code>fastdtw<\/code> (or any DTW algorithm) is a sequence of index pairs <code>(i, j)<\/code> that represent the optimal alignment between two time series. Each pair indicates that element <code>A[i]<\/code> is matched with <code>B[j]<\/code>. By summing the distances between all these matched pairs, the algorithm computes the <strong>optimized cumulative cost<\/strong>\u200a\u2014\u200athe minimum total distance required to warp one sequence to the other.<\/p>\n<figure class=\"wp-block-image aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-80.png?ssl=1\" alt=\"\" class=\"wp-image-603586\" style=\"width:362px;height:auto\"><\/figure>\n<h3 class=\"wp-block-heading\">Modified Dynamic\u00a0Warping<\/h3>\n<p class=\"wp-block-paragraph\">The key challenge when applying <strong>dynamic time warping (DTW)<\/strong> to <strong>dietary data<\/strong> (vs. simple examples like sine waves or fixed-length sequences) lies in the <strong>complexity and variability<\/strong> of real-world eating behaviors. Some challenges and the proposed solution in the paper[1] as a response to each challenge are as follows:<\/p>\n<ol class=\"wp-block-list\">\n<li class=\"wp-block-list-item\">Irregular Time Steps: MDTW accounts for this by explicitly incorporating the time difference in the distance function.<\/li>\n<li class=\"wp-block-list-item\">Multidimensional Nutrients: MDTW supports multidimensional vectors to represent nutrients such as calories, fat etc. and uses a weight matrix to handle differing units and the importance of nutrients,<\/li>\n<li class=\"wp-block-list-item\">Unequal number of meals: MDTW allows for <strong>matching with empty eating events<\/strong>, penalizing skipped or unmatched meals appropriately.<\/li>\n<li class=\"wp-block-list-item\">Time Sensitivity: MDTW includes <strong>a time difference penalty<\/strong>, weighting eating events far apart in time even if the nutrients are similar.<\/li>\n<\/ol>\n<h4 class=\"wp-block-heading\">Eating Occasion Data Representation<\/h4>\n<p class=\"wp-block-paragraph\">According to the modified dynamic time warping proposed in the paper[1], each person\u2019s diet can be thought of as a <strong>sequence of eating events<\/strong>, where each event has:<\/p>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-81.png?ssl=1\" alt=\"\" class=\"wp-image-603587\"><\/figure>\n<p class=\"wp-block-paragraph\">To illustrate how eating records appear in real data, I created <strong>three synthetic dietary profiles<\/strong> only considering calorie consumption\u200a\u2014\u200a<strong>Skipper<\/strong>, <strong>Night Eater<\/strong>, and <strong>Snacker. <\/strong>Let\u2019s assume if we ingest the raw data from an API in this format:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">skipper={\n    'person_id': 'skipper_1',\n    'records': [\n        {'time': 12, 'nutrients': [300]},  # Skipped breakfast, large lunch\n        {'time': 19, 'nutrients': [600]},  # Large dinner\n    ]\n}\nnight_eater={\n    'person_id': 'night_eater_1',\n    'records': [\n        {'time': 9, 'nutrients': [150]},   # Light breakfast\n        {'time': 14, 'nutrients': [250]},  # Small lunch\n        {'time': 22, 'nutrients': [700]},  # Large late dinner\n    ]\n}\nsnacker=  {\n    'person_id': 'snacker_1',\n    'records': [\n        {'time': 8, 'nutrients': [100]},   # Light morning snack\n        {'time': 11, 'nutrients': [150]},  # Late morning snack\n        {'time': 14, 'nutrients': [200]},  # Afternoon snack\n        {'time': 17, 'nutrients': [100]},  # Early evening snack\n        {'time': 21, 'nutrients': [200]},  # Night snack\n    ]\n}\nraw_data = [skipper, night_eater, snacker]<\/code><\/pre>\n<p class=\"wp-block-paragraph\">As suggested in the paper, the nutritional values should be normalized by the total calorie consumptions.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import numpy as np\nimport matplotlib.pyplot as plt\ndef create_time_series_plot(data,save_path=None):\n    plt.figure(figsize=(10, 5))\n    for person,record in data.items():\n        #in case the nutrient vector has more than one dimension\n        data=[[time, float(np.mean(np.array(value)))] for time,value in record.items()]\n\n        time = [item[0] for item in data]\n        nutrient_values = [item[1] for item in data]\n        # Plot the time series\n        plt.plot(time, nutrient_values, label=person, marker='o')\n\n    plt.title('Time Series Plot for Nutrient Data')\n    plt.xlabel('Time')\n    plt.ylabel('Normalized Nutrient Value')\n    plt.legend()\n    plt.grid(True)\n    if save_path:\n        plt.savefig(save_path)\n\ndef prepare_person(person):\n    \n    # Check if all nutrients have same length\n    nutrients_lengths = [len(record['nutrients']) for record in person[\"records\"]]\n    \n    if len(set(nutrients_lengths)) != 1:\n        raise ValueError(f\"Inconsistent nutrient vector lengths for person {person['person_id']}.\")\n\n    sorted_records = sorted(person[\"records\"], key=lambda x: x['time'])\n\n    nutrients = np.stack([np.array(record['nutrients']) for record in sorted_records])\n    total_nutrients = np.sum(nutrients, axis=0)\n\n    # Check to avoid division by zero\n    if np.any(total_nutrients == 0):\n        raise ValueError(f\"Zero total nutrients for person {person['person_id']}.\")\n\n    normalized_nutrients = nutrients \/ total_nutrients\n\n    # Return a dictionary {time: [normalized nutrients]}\n    person_dict = {\n        record['time']: normalized_nutrients[i].tolist()\n        for i, record in enumerate(sorted_records)\n    }\n\n    return person_dict\nprepared_data = {person['person_id']: prepare_person(person) for person in raw_data}\ncreate_time_series_plot(prepared_data)<\/code><\/pre>\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/1UYTCJHV0GoeSh7bopH5Nfg.png?ssl=1\" alt=\"\" class=\"wp-image-603724\"><figcaption class=\"wp-element-caption\">Plot of eating occasion of three different profiles (Image by author)<\/figcaption><\/figure>\n<h4 class=\"wp-block-heading\">Calculation Distance of\u00a0Pairs<\/h4>\n<p class=\"wp-block-paragraph\">The computation of distance measure between pair of individuals are defined in the formula below. The first term represent an Euclidean distance of nutrient vectors whereas the second one takes into account the time penalty.<\/p>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-82.png?ssl=1\" alt=\"\" class=\"wp-image-603588\"><\/figure>\n<p class=\"wp-block-paragraph\">This formula is implemented in the <code>local_distance<\/code> function with the suggested values:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import numpy as np\n\ndef local_distance(eo_i, eo_j,delta=23, beta=1, alpha=2):\n    \"\"\"\n    Calculate the local distance between two events.\n    Args:\n        eo_i (tuple): Event i (time, nutrients).\n        eo_j (tuple): Event j (time, nutrients).\n        delta (float): Time scaling factor.\n        beta (float): Weighting factor for time difference.\n        alpha (float): Exponent for time difference scaling.\n    Returns:\n        float: Local distance.\n    \"\"\"\n    ti, vi = eo_i\n    tj, vj = eo_j\n   \n    vi = np.array(vi)\n    vj = np.array(vj)\n\n    if vi.shape != vj.shape:\n        raise ValueError(\"Mismatch in feature dimensions.\")\n    if np.any(vi &lt; 0) or np.any(vj &lt; 0):\n        raise ValueError(\"Nutrient values must be non-negative.\")\n    if np.any(vi&gt;1 ) or np.any(vj&gt;1):\n        raise ValueError(\"Nutrient values must be in the range [0, 1].\")   \n    W = np.eye(len(vi))  # Assume W = identity for now\n    value_diff = (vi - vj).T @ W @ (vi - vj) \n    time_diff = (np.abs(ti - tj) \/ delta) ** alpha\n    scale = 2 * beta * (vi.T @ W @ vj)\n    distance = value_diff + scale * time_diff\n  \n    return distance<\/code><\/pre>\n<p class=\"wp-block-paragraph\">We construct a local distance matrix <em>deo<\/em>(<em>i<\/em>,<em>j<\/em>) for each pair of individuals being compared. The number of rows and columns in this matrix corresponds to the number of eating occasions for each individual.<\/p>\n<p class=\"wp-block-paragraph\">Once the local distance matrix deo(i,j) is constructed\u200a\u2014\u200acapturing the pairwise distances between all eating occasions of two individuals\u200a\u2014\u200athe next step is to compute the <strong>global cost matrix<\/strong> dER(i,j). This matrix accumulates the minimal alignment cost by considering three possible transitions at each step: matching two eating occasions, skipping an occasion in the first record (aligning to an empty), or skipping an occasion in the second record.<\/p>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-83.png?ssl=1\" alt=\"\" class=\"wp-image-603589\"><\/figure>\n<p class=\"wp-block-paragraph\">To compute the <strong>overall distance<\/strong> between two sequences of eating occasions, we build:<\/p>\n<p class=\"wp-block-paragraph\">A <strong>local distance matrix<\/strong> <code>deo<\/code> filled using <code>local_distance<\/code>.<\/p>\n<ul class=\"wp-block-list\">\n<li class=\"wp-block-list-item\">A <strong>global cost matrix<\/strong> <code>dER<\/code> using dynamic programming, minimizing over:<\/li>\n<li class=\"wp-block-list-item\">Match<\/li>\n<li class=\"wp-block-list-item\">Skip in the first sequence (align to empty)<\/li>\n<li class=\"wp-block-list-item\">Skip in the second sequence<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">These directly implement the recurrence:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import numpy as np\n\ndef mdtw_distance(ER1, ER2, delta=23, beta=1, alpha=2):\n    \"\"\"\n    Calculate the modified DTW distance between two sequences of events.\n    Args:\n        ER1 (list): First sequence of events (time, nutrients).\n        ER2 (list): Second sequence of events (time, nutrients).\n        delta (float): Time scaling factor.\n        beta (float): Weighting factor for time difference.\n        alpha (float): Exponent for time difference scaling.\n    \n    Returns:\n        float: Modified DTW distance.\n    \"\"\"\n    m1 = len(ER1)\n    m2 = len(ER2)\n   \n    # Local distance matrix including matching with empty\n    deo = np.zeros((m1 + 1, m2 + 1))\n\n    for i in range(m1 + 1):\n        for j in range(m2 + 1):\n            if i == 0 and j == 0:\n                deo[i, j] = 0\n            elif i == 0:\n                tj, vj = ER2[j-1]\n                deo[i, j] = np.dot(vj, vj)  \n            elif j == 0:\n                ti, vi = ER1[i-1]\n                deo[i, j] = np.dot(vi, vi)\n            else:\n                deo[i, j]=local_distance(ER1[i-1], ER2[j-1], delta, beta, alpha)\n\n    # # Global cost matrix\n    dER = np.zeros((m1 + 1, m2 + 1))\n    dER[0, 0] = 0\n\n    for i in range(1, m1 + 1):\n        dER[i, 0] = dER[i-1, 0] + deo[i, 0]\n    for j in range(1, m2 + 1):\n        dER[0, j] = dER[0, j-1] + deo[0, j]\n\n    for i in range(1, m1 + 1):\n        for j in range(1, m2 + 1):\n            dER[i, j] = min(\n                dER[i-1, j-1] + deo[i, j],   # Match i and j\n                dER[i-1, j] + deo[i, 0],     # Match i to empty\n                dER[i, j-1] + deo[0, j]      # Match j to empty\n            )\n   \n    \n    return dER[m1, m2]  # Return the final cost\n\nERA = list(prepared_data['skipper_1'].items())\nERB = list(prepared_data['night_eater_1'].items())\ndistance = mdtw_distance(ERA, ERB)\nprint(f\"Distance between skipper_1 and night_eater_1: {distance}\")<\/code><\/pre>\n<h4 class=\"wp-block-heading\">From Pairwise Comparisons to a Distance\u00a0Matrix<\/h4>\n<p class=\"wp-block-paragraph\">Once we define how to calculate the distance between two individuals\u2019 eating patterns using MDTW, the next natural step is to compute distances across the <strong>entire dataset<\/strong>. To do this, we construct a distance matrix where each entry (i,j) represents the MDTW distance between person i and person j.<\/p>\n<p class=\"wp-block-paragraph\">This is implemented in the function below:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import numpy as np\n\ndef calculate_distance_matrix(prepared_data):\n    \"\"\"\n    Calculate the distance matrix for the prepared data.\n    \n    Args:\n        prepared_data (dict): Dictionary containing prepared data for each person.\n        \n    Returns:\n        np.ndarray: Distance matrix.\n    \"\"\"\n    n = len(prepared_data)\n    distance_matrix = np.zeros((n, n))\n    \n    # Compute pairwise distances\n    for i, (id1, records1) in enumerate(prepared_data.items()):\n        for j, (id2, records2) in enumerate(prepared_data.items()):\n            if i &lt; j:  # Only upper triangle\n                print(f\"Calculating distance between {id1} and {id2}\")\n                ER1 = list(records1.items())\n                ER2 = list(records2.items())\n                \n                distance_matrix[i, j] = mdtw_distance(ER1, ER2)\n                distance_matrix[j, i] = distance_matrix[i, j]  # Symmetric matrix\n                \n    return distance_matrix\ndef plot_heatmap(matrix,people_ids,save_path=None):\n    \"\"\"\n    Plot a heatmap of the distance matrix.  \n    Args:\n        matrix (np.ndarray): The distance matrix.\n        title (str): The title of the plot.\n        save_path (str): Path to save the plot. If None, the plot will not be saved.\n    \"\"\"\n    plt.figure(figsize=(8, 6))\n    plt.imshow(matrix, cmap='hot', interpolation='nearest')\n    plt.colorbar()\n  \n    plt.xticks(ticks=range(len(matrix)), labels=people_ids)\n    plt.yticks(ticks=range(len(matrix)), labels=people_ids)\n    plt.xticks(rotation=45)\n    plt.yticks(rotation=45)\n    if save_path:\n        plt.savefig(save_path)\n    plt.title('Distance Matrix Heatmap')\n\ndistance_matrix = calculate_distance_matrix(prepared_data)\nplot_heatmap(distance_matrix, list(prepared_data.keys()), save_path='distance_matrix.png')<\/code><\/pre>\n<p class=\"wp-block-paragraph\">After computing the pairwise Modified Dynamic Time Warping (MDTW) distances, we can <strong>visualize the similarities and differences<\/strong> between individuals\u2019 dietary patterns using a heatmap. Each cell (i,j) in the matrix represents the MDTW distance between person i and person j\u2014 lower values indicate more similar temporal eating profiles.<\/p>\n<p class=\"wp-block-paragraph\">This heatmap offers a <strong>compact and interpretable view<\/strong> of dietary dissimilarities, making it easier to identify clusters of similar eating behaviors.<\/p>\n<p class=\"wp-block-paragraph\">This indicates that <code>skipper_1<\/code> <strong>shares more similarity with <\/strong><code>night_eater_1<\/code> than with <code>snacker_1<\/code>. The reason is that both skipper and night eater have <strong>fewer, larger meals concentrated later in the day<\/strong>, while the snacker distributes smaller meals more evenly across the entire timeline.<\/p>\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-84.png?ssl=1\" alt=\"\" class=\"wp-image-603590\" style=\"width:651px;height:auto\"><figcaption class=\"wp-element-caption\">Distance Matrix Heatmap (Image by author)<\/figcaption><\/figure>\n<h3 class=\"wp-block-heading\"><strong>Clustering Temporal Dietary\u00a0Patterns<\/strong><\/h3>\n<p class=\"wp-block-paragraph\">After calculating the pairwise distances using Modified Dynamic Time Warping (MDTW), we\u2019re left with a distance matrix that reflects how dissimilar each individual\u2019s eating pattern is from the others. But this matrix alone doesn\u2019t tell us much at a glance\u200a\u2014\u200ato reveal structure in the data, we need to go one step further.<\/p>\n<p class=\"wp-block-paragraph\">Before applying any <a href=\"https:\/\/towardsdatascience.com\/tag\/clustering-algorithm\/\" title=\"Clustering Algorithm\">Clustering Algorithm<\/a>, we first need a dataset that reflects realistic dietary behaviors. Since access to large-scale dietary intake datasets can be limited or subject to usage restrictions, I generated synthetic eating event records that simulate diverse daily patterns. Each record represents a person\u2019s calorie intake at specific hours throughout a 24-hour period.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import numpy as np\n\ndef generate_synthetic_data(num_people=5, min_meals=1, max_meals=5,min_calories=200,max_calories=800):\n    \"\"\"\n    Generate synthetic data for a given number of people.\n    Args:\n        num_people (int): Number of people to generate data for.\n        min_meals (int): Minimum number of meals per person.\n        max_meals (int): Maximum number of meals per person.\n        min_calories (int): Minimum calories per meal.\n        max_calories (int): Maximum calories per meal.\n    Returns:\n        list: List of dictionaries containing synthetic data for each person.\n    \"\"\"\n    data = []\n    np.random.seed(42)  # For reproducibility\n    for person_id in range(1, num_people + 1):\n        num_meals = np.random.randint(min_meals, max_meals + 1)  # random number of meals between min and max\n        meal_times = np.sort(np.random.choice(range(24), num_meals, replace=False))  # random times sorted\n\n        raw_calories = np.random.randint(min_calories, max_calories, size=num_meals)  # random calories between min and max\n\n        person_record = {\n            'person_id': f'person_{person_id}',\n            'records': [\n                {'time': float(time), 'nutrients': [float(cal)]} for time, cal in zip(meal_times, raw_calories)\n            ]\n        }\n\n        data.append(person_record)\n    return data\n\nraw_data=generate_synthetic_data(num_people=1000, min_meals=1, max_meals=5,min_calories=200,max_calories=800)\nprepared_data = {person['person_id']: prepare_person(person) for person in raw_data}\ndistance_matrix = calculate_distance_matrix(prepared_data)<\/code><\/pre>\n<h4 class=\"wp-block-heading\">Choosing the Optimal Number of\u00a0Clusters<\/h4>\n<p class=\"wp-block-paragraph\">To determine the appropriate number of clusters for grouping dietary patterns, I evaluated two popular methods: the <strong>Elbow Method<\/strong> and the <strong>Silhouette Score<\/strong>.<\/p>\n<ul class=\"wp-block-list\">\n<li class=\"wp-block-list-item\">The <strong>Elbow Method<\/strong> analyzes the clustering cost (inertia) as the number of clusters increases. As shown in the plot, the cost decreases sharply up to <strong>4 clusters<\/strong>, after which the rate of improvement slows significantly. This \u201celbow\u201d suggests diminishing returns beyond 4 clusters.<\/li>\n<li class=\"wp-block-list-item\">The <strong>Silhouette Score<\/strong>, which measures how well each object lies within its cluster, showed a relatively high score at <strong>4 clusters<\/strong> (\u22480.50), even if it wasn\u2019t the absolute peak.<\/li>\n<\/ul>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-85.png?ssl=1\" alt=\"\" class=\"wp-image-603591\"><figcaption class=\"wp-element-caption\">Optimal number of cluster (Image by author)<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\">The following code computes the clustering cost and silhouette scores for different values of <em>k<\/em> (number of clusters), using the <strong>K-Medoids<\/strong> algorithm and a precomputed distance matrix derived from the MDTW metric:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">from sklearn.metrics import silhouette_score\nfrom sklearn_extra.cluster import KMedoids\nimport matplotlib.pyplot as plt\n\ncosts = []\nsilhouette_scores = []\nfor k in range(2, 10):\n    model = KMedoids(n_clusters=k, metric='precomputed', random_state=42)\n    labels = model.fit_predict(distance_matrix)\n    costs.append(model.inertia_)\n    score = silhouette_score(distance_matrix, model.labels_, metric='precomputed')\n    silhouette_scores.append(score)\n\n# Plot\nks = list(range(2, 10))\nfig, ax1 = plt.subplots(figsize=(8, 5))\n\ncolor1 = 'tab:blue'\nax1.set_xlabel('Number of Clusters (k)')\nax1.set_ylabel('Cost (Inertia)', color=color1)\nax1.plot(ks, costs, marker='o', color=color1, label='Cost')\nax1.tick_params(axis='y', labelcolor=color1)\n\n# Create a second y-axis that shares the same x-axis\nax2 = ax1.twinx()\ncolor2 = 'tab:red'\nax2.set_ylabel('Silhouette Score', color=color2)\nax2.plot(ks, silhouette_scores, marker='s', color=color2, label='Silhouette Score')\nax2.tick_params(axis='y', labelcolor=color2)\n\n# Optional: combine legends\nlines1, labels1 = ax1.get_legend_handles_labels()\nlines2, labels2 = ax2.get_legend_handles_labels()\nax1.legend(lines1 + lines2, labels1 + labels2, loc='upper right')\nax1.vlines(x=4, ymin=min(costs), ymax=max(costs), color='gray', linestyle='--', linewidth=0.5)\n\nplt.title('Cost and Silhouette Score vs Number of Clusters')\nplt.tight_layout()\nplt.savefig('clustering_metrics_comparison.png')\nplt.show()<\/code><\/pre>\n<h4 class=\"wp-block-heading\">Interpreting the Clustered Dietary\u00a0Patterns<\/h4>\n<p class=\"wp-block-paragraph\">Once the optimal number of clusters (<strong>k=4<\/strong>) was determined, each individual in the dataset was assigned to one of these clusters using the K-Medoids model. Now, we need to understand what characterizes each cluster.<\/p>\n<p class=\"wp-block-paragraph\">To do so, I followed the approach suggested in the original MDTW paper [1]: analyzing the <strong>largest eating occasion<\/strong> for every individual, defined by both the <strong>time of day<\/strong> it occurred and the <strong>fraction of total daily intake<\/strong> it represented. This provides insight into <em>when<\/em> people consume the most calories and <em>how much<\/em> they consume during that peak occasion.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\"># Kmedoids clustering with the optimal number of clusters\nfrom sklearn_extra.cluster import KMedoids\nimport seaborn as sns\nimport pandas as pd\n\nk=4\nmodel = KMedoids(n_clusters=k, metric='precomputed', random_state=42)\nlabels = model.fit_predict(distance_matrix)\n\n# Find the time and fraction of their largest eating occasion\ndef get_largest_event(record):\n    total = sum(v[0] for v in record.values())\n    largest_time, largest_value = max(record.items(), key=lambda x: x[1][0])\n    fractional_value = largest_value[0] \/ total if total &gt; 0 else 0\n    return largest_time, fractional_value\n\n# Create a largest meal data per cluster\ndata_per_cluster = {i: [] for i in range(k)}\nfor i, person_id in enumerate(prepared_data.keys()):\n    cluster_id = labels[i]\n    t, v = get_largest_event(prepared_data[person_id])\n    data_per_cluster[cluster_id].append((t, v))\n\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport pandas as pd\n\n# Convert to pandas DataFrame\nrows = []\nfor cluster_id, values in data_per_cluster.items():\n    for hour, fraction in values:\n        rows.append({\"Hour\": hour, \"Fraction\": fraction, \"Cluster\": f\"Cluster {cluster_id}\"})\ndf = pd.DataFrame(rows)\nplt.figure(figsize=(10, 6))\nsns.scatterplot(data=df, x=\"Hour\", y=\"Fraction\", hue=\"Cluster\", palette=\"tab10\")\nplt.title(\"Eating Events Across Clusters\")\nplt.xlabel(\"Hour of Day\")\nplt.ylabel(\"Fraction of Daily Intake (largest meal)\")\nplt.grid(True)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-86.png?ssl=1\" alt=\"\" class=\"wp-image-603592\"><figcaption class=\"wp-element-caption\">Each point represents an individual\u2019s largest eating event (Image by author)<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\"><strong>While the scatter plot offers a broad overview, a more detailed understanding of each cluster\u2019s eating behavior can be gained by examining their joint distributions.<\/strong><br \/>By plotting the joint histogram of the hour and fraction of daily intake for the largest meal, we can identify characteristic patterns, using the code below:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\"># Plot each cluster using seaborn.jointplot\nfor cluster_label in df['Cluster'].unique():\n    cluster_data = df[df['Cluster'] == cluster_label]\n    g = sns.jointplot(\n        data=cluster_data,\n        x=\"Hour\",\n        y=\"Fraction\",\n        kind=\"scatter\",\n        height=6,\n        color=sns.color_palette(\"deep\")[int(cluster_label.split()[-1])]\n    )\n    g.fig.suptitle(cluster_label, fontsize=14)\n    g.set_axis_labels(\"Hour of Day\", \"Fraction of Daily Intake (largest meal)\", fontsize=12)\n    g.fig.tight_layout()\n    g.fig.subplots_adjust(top=0.95)  # adjust title spacing\n    plt.show()<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-87.png?ssl=1\" alt=\"\" class=\"wp-image-603593\"><figcaption class=\"wp-element-caption\">Each subplot represents the joint distribution of time (x-axis) and fractional calorie intake (y-axis) for individuals within a cluster. Higher densities indicate common timings and portion sizes of the largest meals. (Image by author)<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\">To understand how individuals were distributed across clusters, I visualized the number of people assigned to each cluster. The bar plot below shows the frequency of individuals grouped by their temporal dietary pattern. This helps assess whether certain eating behaviors\u200a\u2014\u200asuch as skipping meals, late-night eating, or frequent snacking\u200a\u2014\u200aare more prevalent in the population.<\/p>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/05\/image-88.png?ssl=1\" alt=\"\" class=\"wp-image-603594\"><figcaption class=\"wp-element-caption\">Histogram showing the number of individuals assigned to each dietary pattern cluster (Image by author)<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\"><strong>Based on the joint distribution plots<\/strong>, distinct temporal dietary behaviors emerge across clusters:<\/p>\n<p class=\"wp-block-paragraph\"><strong>Cluster 0 <\/strong>(Flexible or Irregular Eater) reveals a <strong>broad dispersion<\/strong> of the largest eating occasions across both the <strong>24-hour day<\/strong> and the <strong>fraction of daily caloric intake<\/strong>.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Cluster 1 <\/strong>(Frequent Light Eaters) displays a <strong>more evenly distributed eating pattern<\/strong>, where no single eating occasion exceeds <strong>30% of the total daily intake<\/strong>, reflecting frequent but smaller meals throughout the day. This is the cluster that most likely represents <strong>\u201cnormal eaters\u201d<\/strong>\u200a\u2014\u200athose who consume <strong>three relatively balanced meals spread throughout the day.<\/strong> That is because of low variance in timing and fraction per eating event.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Cluster 2 <\/strong>(Early Heavy Eaters) is defined by a very <strong>distinct and consistent pattern<\/strong>: individuals in this group consume <strong>almost their entire daily caloric intake (close to 100%) in a single meal<\/strong>, predominantly during the <strong>early hours of the day (midnight to noon)<\/strong>.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Cluster 3<\/strong> (Late Night Heavy Eaters) is characterized by individuals who consume <strong>nearly all of their daily calories in a single meal during the late evening or night hours (between 6 PM and midnight)<\/strong>. Like Cluster 2, this group exhibits a <strong>unimodal eating pattern<\/strong> with a <strong>very high fractional intake (~1.0)<\/strong>, indicating that most members eat <strong>once per day<\/strong>, but unlike Cluster 2, their eating window is significantly delayed.<\/p>\n<h3 class=\"wp-block-heading\">CONCLUSION <\/h3>\n<p class=\"wp-block-paragraph\">In this project, I explored how <strong>Modified <a href=\"https:\/\/towardsdatascience.com\/tag\/dynamic-time-warping\/\" title=\"Dynamic Time Warping\">Dynamic Time Warping<\/a> (MDTW)<\/strong> can help uncover temporal dietary patterns\u200a\u2014\u200afocusing not just on what we eat, but <strong>when<\/strong> and <strong>how much<\/strong>. Using <strong>synthetic data<\/strong> to simulate realistic eating behaviors, I demonstrated how MDTW can cluster individuals into distinct profiles like irregular or flexible eaters, frequent light eaters, early heavy eaters and later night eaters based on the timing and magnitude of their meals.<\/p>\n<p class=\"wp-block-paragraph\">While the results show that MDTW combined with <strong>K-Medoids<\/strong> can reveal meaningful patterns in eating behaviors, this approach isn\u2019t without its challenges. Since the dataset was synthetically generated and clustering was based on a single initialization, there are several caveats worth noting:<\/p>\n<ul class=\"wp-block-list\">\n<li class=\"wp-block-list-item\">The clusters appear messy, possibly because the synthetic data lacks strong, naturally separable patterns \u2014 especially if meal times and calorie distributions are too uniform.<\/li>\n<li class=\"wp-block-list-item\">Some clusters overlap significantly, particularly <strong>Cluster 0<\/strong> and <strong>Cluster 1<\/strong>, making it harder to distinguish between truly different behaviors.<\/li>\n<li class=\"wp-block-list-item\">Without labeled data or expected ground truth, evaluating cluster quality is difficult. A potential improvement would be to inject known patterns into the dataset to test whether the clustering algorithm can reliably recover them.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">Despite these limitations, this work shows how a nuanced distance metric\u200a\u2014\u200adesigned for irregular, real-life patterns\u200a\u2014\u200acan surface insights traditional tools may overlook. The methodology can be extended to <strong>personalized health monitoring<\/strong>, or any domain where <strong>when things happen<\/strong> matters just as much as <strong>what happens<\/strong>.<\/p>\n<p class=\"wp-block-paragraph\">I\u2019d love to hear your thoughts on this project\u200a\u2014\u200awhether it\u2019s feedback, questions, or ideas for where MDTW could be applied next. This is very much a work in progress, and I\u2019m always excited to learn from others.<\/p>\n<p class=\"wp-block-paragraph\">If you found this useful, have ideas for improvements, or want to collaborate, feel free to open an issue or send a Pull Request on GitHub. Contributions are more than welcome!<\/p>\n<p class=\"wp-block-paragraph\">Thanks so much for reading all the way to the end\u200a\u2014\u200ait really means a lot.<\/p>\n<p class=\"wp-block-paragraph\">Code on GitHub\u00a0: <a href=\"https:\/\/github.com\/YagmurGULEC\/mdtw-time-series-clustering\" rel=\"noreferrer noopener\" target=\"_blank\">https:\/\/github.com\/YagmurGULEC\/mdtw-time-series-clustering<\/a><\/p>\n<h3 class=\"wp-block-heading\">REFERENCES<\/h3>\n<p class=\"wp-block-paragraph\">[1] Khanna, Nitin, et al. \u201cModified dynamic time warping (MDTW) for estimating temporal dietary patterns.\u201d <em>2017 IEEE Global Conference on Signal and Information Processing (GlobalSIP)<\/em>. IEEE, 2017.<\/p>\n<p>The post <a href=\"https:\/\/towardsdatascience.com\/clustering-eating-behaviors-in-time-a-machine-learning-approach-to-preventive-health\/\">Clustering Eating Behaviors in Time: A Machine Learning Approach to Preventive Health<\/a> appeared first on <a href=\"https:\/\/towardsdatascience.com\/\">Towards Data Science<\/a>.<\/p>\n<\/div>\n<p> \t<BR><br \/>\n <BR><\/BR><br \/>\n    Yagmur Gulec<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n<a href=\"https:\/\/towardsdatascience.com\/clustering-eating-behaviors-in-time-a-machine-learning-approach-to-preventive-health\/\">Go to original source<\/a><br \/>\n \t<BR><br \/>\n <BR><\/BR><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Clustering Eating Behaviors in Time: A Machine Learning Approach to Preventive Health It\u2019s well known that\u00a0what\u00a0we eat matters \u2014 but what if\u00a0when\u00a0and\u00a0how often\u00a0we eat matters just as much? In the midst of ongoing scientific debate around the benefits of\u00a0intermittent fasting, this question becomes even more intriguing. As someone passionate about machine learning and healthy living, [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[62,2623,67,2624,70,157],"tags":[2626,2625,15],"class_list":["post-3695","post","type-post","status-publish","format-standard","hentry","category-aimldsaimlds","category-clustering-algorithm","category-deep-dives","category-dynamic-time-warping","category-machine-learning","category-python","tag-eat","tag-mdtw","tag-time"],"_links":{"self":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/3695"}],"collection":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/comments?post=3695"}],"version-history":[{"count":0,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/3695\/revisions"}],"wp:attachment":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/media?parent=3695"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/categories?post=3695"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/tags?post=3695"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}