{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "cYRMal2svoW6"
},
"source": [
"# **Southern Ocean Mixed Layer Depth Estimation from ARGO Floats — Regression Method of Courtois et al. (2017)** #"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For an interactive version of this page please visit the Google Colab: \n",
"[ Open in Google Colab ](https://colab.research.google.com/drive/19G3F5qUWNLjbLBuTkznv4jhKzncPuTga)
\n",
"(To open link in new tab press Ctrl + click)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Alternatively this notebook can be opened with Binder by following the link:\n",
"[Southern Ocean Mixed Layer Depth Estimation from ARGO Floats — Regression Method of Courtois et al. (2017)](https://mybinder.org/v2/gh/s4oceanice/literacy.s4oceanice/main?urlpath=%2Fdoc%2Ftree%2Fnotebooks_binder%2Foceanice_mixed_layer_depth.ipynb)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "zKHwIUj4vn7k"
},
"source": [
"**Purpose**\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1kmpeJi28JOV"
},
"source": [
"The Mixed Layer Depth (MLD) marks the upper ocean layer that is stirred and blended by winds, waves, and currents. It is a key property for many reasons:\n",
"\n",
"1. **Climate & Heat Storage**: Controls heat and gas exchange between ocean and atmosphere.\n",
"\n",
"2. **Marine Life**: Influences nutrient supply and light availability for phytoplankton growth.\n",
"\n",
"3. **Carbon Cycle**: Regulates CO₂ uptake and long-term storage in the ocean interior.\n",
"\n",
"4. **Ocean Circulation**: Contributes to water mass formation and global current systems.\n",
"\n",
"In the Southern Ocean, MLD variability is central to understanding climate change impacts and ecosystem dynamics.\n",
"\n",
"This notebook provides interactive tools to visualize and analyze MLD estimates from ARGO profiling floats. Users can:\n",
"\n",
"* Select specific float platforms and time periods.\n",
"\n",
"* View temperature–depth profiles and identify the MLD using a regression-based method.\n",
"\n",
"* Map monthly average MLDs across multiple floats.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "MDRIS4Ii8DJ0"
},
"source": [
"**Data sources**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "4GscmbGR8Jx9"
},
"source": [
"**ARGO floats** are autonomous, free-drifting instruments used for large-scale ocean monitoring. Each float:\n",
"\n",
"* Cycles vertically from the surface to depths of up to ~2,000 m.\n",
"\n",
"* Measures temperature, salinity, and sometimes biogeochemical parameters.\n",
"\n",
"* Transmits data via satellite when at the surface.\n",
"\n",
"* Operates for 4–5 years, collecting hundreds of profiles during its lifetime.\n",
"\n",
"The global ARGO program maintains a network of ~4,000 floats worldwide. In the **Southern Ocean**, these floats provide year-round coverage in otherwise inaccessible regions, making them essential for climate and oceanographic research.\n",
"\n",
"The dataset used in this notebook comes from https://er1.s4oceanice.eu/erddap/tabledap/ARGO_FLOATS_OCEANICE.html. It includes time, latitude, longitude, pressure (converted to depth in meters), and temperature profiles. The analysis here focuses on the period **December 2023 – March 2024**, but users can adjust the query to other intervals."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "dhgXKqHgvw_e"
},
"source": [
"**Instructions to use this Notebook**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "DpjFwRRfv2c-"
},
"source": [
"Run each code cell in order by clicking the **Play button** (▶️) on the left of each grey code block. This ensures all features execute properly."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "xjFxpeyRvw1V"
},
"source": [
"**Explaining the code**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "J_fcIUuknt4k"
},
"source": [
"**Method Note**\n",
"\n",
"The MLD estimation algorithm used here follows **Courtois et al. 2017**, who proposed a simplified regression-based method inspired by **Holte & Talley 2009**.\n",
"\n",
"* Two linear regressions are fit: one in the **mixed layer** (≤100 m) and one in the **thermocline** (150–500 m).\n",
"\n",
"* The **intersection** of these regressions defines the MLD.\n",
"\n",
"* Compared to Holte & Talley’s original multi-criterion approach, this version is computationally lighter and well-suited to analyzing large ARGO datasets in regions with deep convection, such as the Southern Ocean."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "DSciOJn1wXLg"
},
"source": [
"**1. Notebook Setup and ARGO Float Platform Data Source Definition**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "fs2h5ZIJwYG5"
},
"source": [
"This section imports all the necessary Python libraries for data handling, statistical analysis, mapping, and interactive widget creation. It also sets the URLs for accessing ARGO float platform information and associated time records from the OCEAN ICE ERDDAP server."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "96a35157"
},
"source": [
"The following libraries are used in this notebook:\n",
"\n",
"* **Data Acquisition & Processing**: [pandas](https://pandas.pydata.org/docs/), [numpy](https://numpy.org/doc/), [datetime.datetime](https://docs.python.org/3/library/datetime.html#datetime.datetime), [os](https://docs.python.org/3/library/os.html)\n",
"* **Visualization & Mapping**: [matplotlib.pyplot](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.html), [scipy.stats.linregress](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.linregress.html), [folium](https://python-visualization.github.io/folium/), [folium.plugins.MarkerCluster](https://python-visualization.github.io/folium/plugins.html#folium.plugins.MarkerCluster)\n",
"* **Interactive Data Exploration**: [ipywidgets](https://ipywidgets.readthedocs.io/en/latest/index.html)\n",
"* **Output & Presentation**: [warnings](https://docs.python.org/3/library/warnings.html), [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "9TH4rTgNrmh3"
},
"outputs": [],
"source": [
"# @title\n",
"import numpy as np\n",
"from folium.plugins import MarkerCluster\n",
"import folium\n",
"import warnings\n",
"import os\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import linregress\n",
"import pandas as pd\n",
"from datetime import datetime\n",
"from ipywidgets import (\n",
" FloatSlider,\n",
" Text,\n",
" HBox,\n",
" Layout,\n",
" Output,\n",
" VBox,\n",
" HBox,\n",
" HTML,\n",
" Label,\n",
" Dropdown,\n",
" SelectionSlider,\n",
" Button\n",
")\n",
"from IPython.display import display, FileLink, HTML\n",
"\n",
"platform_url = 'https://er1.s4oceanice.eu/erddap/tabledap/ARGO_FLOATS_OCEANICE.csv?PLATFORMCODE'\n",
"time_plat_url = 'https://er1.s4oceanice.eu/erddap/tabledap/ARGO_FLOATS_OCEANICE.csv?PLATFORMCODE%2Ctime&time%3E=2023-12-19T22%3A25%3A00Z&time%3C=2024-03-07T19%3A23%3A20Z'"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "YOiXx37-yMb6"
},
"source": [
"**2. Interactive ARGO Float Profile Viewer and MLD Estimator**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Vs1g_tA3yNg7"
},
"source": [
"This tool lets the user browse **temperature–depth** profiles by platform and date.\n",
"\n",
"* Data are retrieved from ERDDAP and pressure is converted to depth (1 dbar ≈ 1.0047 m).\n",
"\n",
"* MLD is then estimated following Courtois et al. (2017)\n",
"\n",
"* The resulting profile is plotted with regression lines for the mixed layer and thermocline, and a red horizontal line marking the estimated MLD."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 81,
"referenced_widgets": [
"166b6728a71a40509e4fb551deae57f5",
"c5339e1b01bd4f8ca0de25a40365079b",
"46e9140a19fc49dabb424ef31a4eb5bf",
"ccee0254fa7943d98fe6de5db310093b",
"9de86c1a8e0c4d85bad1bdfd8ac657ac",
"90e18a6ad99e4b519a1abbda1ff1ef16",
"a05d1c7c3b03441a82673893e305a2f6",
"eebc3570a05b44b3be5b5ffc77b542c4",
"9df09df0470b4a0facd9512ad076282d",
"df195cad0ef640beb4c1a7364f930f82",
"7305128ac9694878998f40287b33f6d4",
"403c64af2bea44eea56be4b29e9f9604",
"6c0932de8edc404186847cd850ab9809",
"d1e63588b6584df7aa49671bd6ed3fb8",
"f88447dbe91645ac904cb798d1ba6559",
"55f9bed6000a488ebc7bebe1c2024f13",
"ab129b4e736c44239809c56d9c9f525d",
"435aba90571e4104bedc378ca3afaf58",
"376151c41dbd43eabbf6152d365bdb62",
"83dca114fd304f86a4cb83b8d8d6a7de"
]
},
"id": "Kzri2yqHtSR2",
"outputId": "d2d6690e-f8ca-47f9-cfbb-3b8bb0ada271"
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "166b6728a71a40509e4fb551deae57f5",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(HBox(children=(Label(value='Select a platform'), Dropdown(options=(np.int64(1902687), np.int64(…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# @title\n",
"# Read the data from the platform URL, skipping the first row\n",
"platforms_df = pd.read_csv(platform_url)\n",
"\n",
"# Get unique platform codes and sort them\n",
"unique_platforms = sorted(platforms_df['PLATFORMCODE'].unique())\n",
"\n",
"# Create and display the platform dropdown\n",
"platform_dropdown = Dropdown(\n",
" options=unique_platforms,\n",
" disabled=False,\n",
")\n",
"\n",
"# Read the data from the time_plat_url, skipping the first row\n",
"time_df = pd.read_csv(time_plat_url, skiprows=[1])\n",
"\n",
"# Convert 'time' column to datetime objects\n",
"time_df['time'] = pd.to_datetime(time_df['time'])\n",
"\n",
"# Create the date dropdown (options will be updated dynamically)\n",
"date_dropdown = Dropdown(\n",
" options=[datetime.now()], # Start with a placeholder option\n",
" disabled=False,\n",
")\n",
"\n",
"# Output widget to display the date dropdown and plot\n",
"date_output_box = Output()\n",
"\n",
"# Output widget for the plot\n",
"plot_output = Output()\n",
"\n",
"# Function to estimate MLD (moved from cell Obn6S-Tyo83p)\n",
"def estimate_mld(depth, theta, ml_limit=100, tc_start=150, tc_end=500):\n",
" # Ensure depth and theta are sorted by depth for correct slicing\n",
" sorted_indices = np.argsort(depth)\n",
" depth = depth[sorted_indices]\n",
" theta = theta[sorted_indices]\n",
"\n",
" # Fitting nello strato misto\n",
" ml_indices = depth <= ml_limit\n",
" ml_depth = depth[ml_indices]\n",
" ml_theta = theta[ml_indices]\n",
" # Check if there's enough data points for linear regression\n",
" if len(ml_depth) < 2:\n",
" slope_ml, intercept_ml = np.nan, np.nan\n",
" else:\n",
" slope_ml, intercept_ml, *_ = linregress(ml_depth, ml_theta)\n",
"\n",
" # Fitting nella termoclina\n",
" tc_indices = (depth >= tc_start) & (depth <= tc_end)\n",
" tc_depth = depth[tc_indices]\n",
" tc_theta = theta[tc_indices]\n",
" # Check if there's enough data points for linear regression\n",
" if len(tc_depth) < 2:\n",
" slope_tc, intercept_tc = np.nan, np.nan\n",
" else:\n",
" slope_tc, intercept_tc, *_ = linregress(tc_depth, tc_theta)\n",
"\n",
" # Intersezione delle rette\n",
" mld = np.nan # Initialize MLD as NaN\n",
" if not np.isnan(slope_ml) and not np.isnan(slope_tc) and slope_ml != slope_tc:\n",
" mld = (intercept_tc - intercept_ml) / (slope_ml - slope_tc)\n",
" # Ensure MLD is within the range of the data used for fitting\n",
" # Use min/max of the relevant data for robust range check\n",
" valid_depths = np.concatenate([ml_depth, tc_depth])\n",
" if len(valid_depths) > 0 and (mld < np.min(valid_depths) or mld > np.max(valid_depths)):\n",
" mld = np.nan # Invalidate MLD if it's outside the fitting range\n",
"\n",
"\n",
" return mld, (slope_ml, intercept_ml), (slope_tc, intercept_tc)\n",
"\n",
"\n",
"# Function to update date dropdown options and plot based on selected platform\n",
"def update_widgets_and_plot(*args):\n",
" selected_platform = platform_dropdown.value\n",
" if selected_platform is not None:\n",
" # Filter time_df for the selected platform and get unique dates\n",
" platform_dates_df = time_df[time_df['PLATFORMCODE'] == selected_platform]\n",
" unique_times = sorted(platform_dates_df['time'].unique())\n",
"\n",
" # Update dropdown options\n",
" with date_output_box:\n",
" date_output_box.clear_output()\n",
" if unique_times:\n",
" date_dropdown.options = unique_times\n",
" date_dropdown.value = unique_times[0] # Set default value if options are available\n",
" display(HBox([Label('Select a date'), date_dropdown])) # Display the label and dropdown in an HBox\n",
" else:\n",
" date_dropdown.options = [datetime.now()] # Reset to placeholder if no dates\n",
" date_dropdown.value = datetime.now() # Set default value to placeholder\n",
" display(HBox([Label('Select a date'), date_dropdown])) # Display the label and dropdown in an HBox\n",
"\n",
" # Now, trigger plot update based on the new dropdown value\n",
" update_plot()\n",
"\n",
"# Function to update the plot when the dropdown value changes\n",
"def update_plot(*args):\n",
" global api_df # Declare api_df as global\n",
" selected_platform = platform_dropdown.value\n",
" selected_time = date_dropdown.value\n",
"\n",
" if selected_platform is not None and selected_time is not None:\n",
" # Format the selected time to the required URL format (YYYY-MM-DDTHH%3AMM%3ASSZ)\n",
" formatted_time = selected_time.strftime('%Y-%m-%dT%H%%3A%M%%3A%SZ')\n",
"\n",
" # Construct the URL with selected values\n",
" api_url = f'https://er1.s4oceanice.eu/erddap/tabledap/ARGO_FLOATS_OCEANICE.csv?time%2Clatitude%2Clongitude%2CPRESS%2CTEMP&PLATFORMCODE=%22{selected_platform}%22&time%3E={formatted_time}&time%3C={formatted_time}'\n",
"\n",
" try:\n",
" # Read the data from the URL into a DataFrame\n",
" api_df = pd.read_csv(api_url)\n",
"\n",
" # Convert 'PRESS' from decibars to depth in meters (approx. 1 dbar = 1.0047 m seawater)\n",
" api_df = api_df.iloc[1:].copy() # Start from the second row and create a copy\n",
" api_df['PRESS (decibar)'] = pd.to_numeric(api_df['PRESS'])\n",
" api_df['DEPTH (m)'] = api_df['PRESS (decibar)'] * 1.0047\n",
" api_df['TEMP (Degree_C)'] = pd.to_numeric(api_df['TEMP'])\n",
"\n",
" # Drop the original columns\n",
" api_df = api_df.drop(columns=['PRESS', 'TEMP'])\n",
"\n",
" # Ensure data is numeric\n",
" real_depth = api_df['DEPTH (m)'].values.astype(float)\n",
" real_theta = api_df['TEMP (Degree_C)'].values.astype(float)\n",
"\n",
" # MLD calculation\n",
" mld, (slope_ml, intercept_ml), (slope_tc, intercept_tc) = estimate_mld(real_depth, real_theta)\n",
"\n",
" # Calculate fitting lines for plotting\n",
" theta_ml_fit = slope_ml * real_depth + intercept_ml\n",
" theta_tc_fit = slope_tc * real_depth + intercept_tc\n",
"\n",
" # Plot rendering\n",
" with plot_output:\n",
" plot_output.clear_output(wait=True)\n",
" plt.figure(figsize=(6, 10))\n",
" plt.plot(real_theta, real_depth, label='Profile θ')\n",
"\n",
" # Plot fitting lines only if slopes and intercepts are not NaN\n",
" if not np.isnan(slope_ml) and not np.isnan(intercept_ml):\n",
" plt.plot(theta_ml_fit, real_depth, '--', label='Fitting ML')\n",
"\n",
" if not np.isnan(slope_tc) and not np.isnan(intercept_tc):\n",
" plt.plot(theta_tc_fit, real_depth, '--', label='Fitting termocline')\n",
"\n",
" # Plot MLD line only if MLD is not NaN\n",
" if not np.isnan(mld):\n",
" plt.axhline(mld, color='red', linestyle='-', label=f'Estimated MLD ≈ {mld:.1f} m')\n",
"\n",
" plt.gca().invert_yaxis()\n",
" plt.xlabel('Potential temperature (°C)')\n",
" plt.ylabel('Depth (m)')\n",
" plt.title(f'Estimation of MLD by platform {selected_platform} as at {selected_time.strftime(\"%Y-%m-%d %H:%M:%S\")}')\n",
" plt.legend()\n",
" plt.grid(True)\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" except Exception as e:\n",
" with plot_output:\n",
" plot_output.clear_output(wait=True)\n",
" print(f\"Error fetching data or generating plot: {e}\")\n",
"\n",
"# Observe changes in the platform dropdown and update the date dropdown and plot\n",
"platform_dropdown.observe(update_widgets_and_plot, names='value')\n",
"\n",
"# Observe changes in the date dropdown and update the plot\n",
"date_dropdown.observe(update_plot, names='value')\n",
"\n",
"# Display the dropdowns and the output widget boxes\n",
"display(VBox([HBox([Label('Select a platform'), platform_dropdown]),\n",
" date_output_box]))\n",
"\n",
"# Initial update of the widgets and plot\n",
"update_widgets_and_plot()"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ysaSETUK1Nof"
},
"source": [
"**3. Batch MLD Computation and Monthly Aggregation**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ZnBVmZB91O-w"
},
"source": [
"This section processes **all ARGO float profiles** from the selected date range:\n",
"\n",
"* Data are cleaned and converted (pressure → depth).\n",
"\n",
"* * The `estimate_mld` function computes the MLD for each profile.\n",
"\n",
"* Each result is paired with geographic coordinates and timestamps.\n",
"\n",
"* Monthly averages of MLD are calculated for each platform and location.\n",
"\n",
"* arker sizes for later maps are scaled according to MLD ranges."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 1000,
"referenced_widgets": [
"be7974ccbabe4193883e2efb1c67f4f3",
"acf53ff7db5c45458e53316c0152e554"
]
},
"id": "tnqIcCqX5I6a",
"outputId": "10ed8a14-41ca-4a07-a86c-37eccf0bafa6"
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "be7974ccbabe4193883e2efb1c67f4f3",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Output()"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# @title\n",
"display(plot_output)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "NOruzBnY6cEP"
},
"source": [
"**4. Display Retrieved Profile Data**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "sYawW3ha6PW0"
},
"source": [
"This section allows direct inspection of raw values, calculated depths, and converted temperatures before further analysis or visualization."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 423
},
"id": "eb094b3d",
"outputId": "b71f7c66-bac0-485f-f76d-b2497bd0bd71"
},
"outputs": [
{
"data": {
"application/vnd.google.colaboratory.intrinsic+json": {
"repr_error": "0",
"type": "dataframe",
"variable_name": "api_df"
},
"text/html": [
"\n",
"
\n", " | time | \n", "latitude | \n", "longitude | \n", "PRESS (decibar) | \n", "DEPTH (m) | \n", "TEMP (Degree_C) | \n", "
---|---|---|---|---|---|---|
1 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "14.1 | \n", "14.16627 | \n", "0.094 | \n", "
2 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "23.9 | \n", "24.01233 | \n", "0.087 | \n", "
3 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "34.0 | \n", "34.15980 | \n", "0.028 | \n", "
4 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "44.1 | \n", "44.30727 | \n", "-0.173 | \n", "
5 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "54.6 | \n", "54.85662 | \n", "-0.783 | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
90 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "904.3 | \n", "908.55021 | \n", "1.133 | \n", "
91 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "914.2 | \n", "918.49674 | \n", "1.134 | \n", "
92 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "924.2 | \n", "928.54374 | \n", "1.134 | \n", "
93 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "934.2 | \n", "938.59074 | \n", "1.134 | \n", "
94 | \n", "2024-01-12T00:33:00Z | \n", "-74.85373 | \n", "-102.42796666666666 | \n", "941.7 | \n", "946.12599 | \n", "1.135 | \n", "
94 rows × 6 columns
\n", "