Last active
January 27, 2026 20:32
-
-
Save jdbcode/43383d9dc28bf71d4f5d9449f17cfc04 to your computer and use it in GitHub Desktop.
earth_engine_noncommercial_eecu_monitor.ipynb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "nbformat": 4, | |
| "nbformat_minor": 0, | |
| "metadata": { | |
| "colab": { | |
| "provenance": [], | |
| "include_colab_link": true | |
| }, | |
| "kernelspec": { | |
| "name": "python3", | |
| "display_name": "Python 3" | |
| }, | |
| "language_info": { | |
| "name": "python" | |
| } | |
| }, | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "view-in-github", | |
| "colab_type": "text" | |
| }, | |
| "source": [ | |
| "<a href=\"https://colab.research.google.com/gist/jdbcode/43383d9dc28bf71d4f5d9449f17cfc04/earth_engine_noncommercial_eecu_monitor.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "source": [ | |
| "**Disclaimer**: This notebook aggregates and visualizes data from [Cloud Monitoring](https://docs.cloud.google.com/monitoring/docs) for informational purposes only. It is not an official Google product and the methods used for aggregation may not reflect official metric calculations." | |
| ], | |
| "metadata": { | |
| "id": "1zAWLpRRPlNA" | |
| } | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": [ | |
| "# @title 🌍 Earth Engine Noncommercial EECU Monitor\n", | |
| "# @markdown Use this notebook to track daily and cumulative monthly EECU usage.\n", | |
| "# @markdown ### **Instructions**\n", | |
| "# @markdown 1. Enter your **Project ID**.\n", | |
| "# @markdown 2. Select **User Tier** (Optional: Select \"None\" for raw usage, or a tier for quota comparisons).\n", | |
| "# @markdown 3. Run the cell to check your status.\n", | |
| "\n", | |
| "import pandas as pd\n", | |
| "import plotly.graph_objects as go\n", | |
| "from plotly.subplots import make_subplots\n", | |
| "import time\n", | |
| "import calendar\n", | |
| "from datetime import datetime, timedelta, timezone\n", | |
| "from google.colab import auth\n", | |
| "from google.cloud import monitoring_v3\n", | |
| "from IPython.display import display, Markdown\n", | |
| "\n", | |
| "# --- USER CONFIGURATION ---\n", | |
| "PROJECT_ID = \"ee-yourproject\" # @param {type:\"string\"}\n", | |
| "SHOW_QUOTA_TIER = \"Community\" # @param [\"None\", \"Community\", \"Contributor\", \"Partner\"]\n", | |
| "MONTHS_TO_SHOW = 12 # @param {type:\"slider\", min:1, max:12, step:1}\n", | |
| "\n", | |
| "# Define limits based on tier\n", | |
| "TIER_LIMITS = {\n", | |
| " \"Community\": 150,\n", | |
| " \"Contributor\": 1000,\n", | |
| " \"Partner\": 100000\n", | |
| "}\n", | |
| "QUOTA_LIMIT = TIER_LIMITS.get(SHOW_QUOTA_TIER)\n", | |
| "\n", | |
| "# Authenticate\n", | |
| "try:\n", | |
| " auth.authenticate_user()\n", | |
| "except:\n", | |
| " pass\n", | |
| "\n", | |
| "def get_eecu_data(project_id, months_back):\n", | |
| " if not project_id or project_id == \"YOUR_PROJECT_ID_HERE\":\n", | |
| " return pd.DataFrame()\n", | |
| "\n", | |
| " client = monitoring_v3.MetricServiceClient()\n", | |
| " project_name = f\"projects/{project_id}\"\n", | |
| "\n", | |
| " # --- 1. DEFINE TIME WINDOW (CALENDAR ALIGNED) ---\n", | |
| " now = datetime.now(timezone.utc)\n", | |
| "\n", | |
| " # Calculate start date (1st of the target month)\n", | |
| " # Logic: If months_back=1, we want start date = 1st of current month.\n", | |
| " # So we subtract (months_back - 1).\n", | |
| " start_year = now.year\n", | |
| " start_month = now.month - (months_back - 1)\n", | |
| "\n", | |
| " while start_month <= 0:\n", | |
| " start_month += 12\n", | |
| " start_year -= 1\n", | |
| " start_time = datetime(start_year, start_month, 1, tzinfo=timezone.utc)\n", | |
| "\n", | |
| " interval = monitoring_v3.TimeInterval({\n", | |
| " \"end_time\": {\"seconds\": int(now.timestamp()), \"nanos\": 0},\n", | |
| " \"start_time\": {\"seconds\": int(start_time.timestamp()), \"nanos\": 0}\n", | |
| " })\n", | |
| "\n", | |
| " try:\n", | |
| " results = client.list_time_series(\n", | |
| " request={\n", | |
| " \"name\": project_name,\n", | |
| " \"filter\": 'metric.type = \"earthengine.googleapis.com/project/cpu/usage_time\"',\n", | |
| " \"interval\": interval,\n", | |
| " \"view\": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL,\n", | |
| " \"aggregation\": {\n", | |
| " \"alignment_period\": {\"seconds\": 86400},\n", | |
| " \"per_series_aligner\": monitoring_v3.Aggregation.Aligner.ALIGN_SUM,\n", | |
| " \"cross_series_reducer\": monitoring_v3.Aggregation.Reducer.REDUCE_SUM\n", | |
| " }\n", | |
| " }\n", | |
| " )\n", | |
| " except Exception as e:\n", | |
| " print(f\"Error fetching data: {e}\")\n", | |
| " return pd.DataFrame()\n", | |
| "\n", | |
| " data = []\n", | |
| " for result in results:\n", | |
| " for point in result.points:\n", | |
| " data.append({\n", | |
| " \"date\": point.interval.end_time,\n", | |
| " \"daily_hours\": point.value.double_value / 3600\n", | |
| " })\n", | |
| "\n", | |
| " if not data:\n", | |
| " return pd.DataFrame(columns=[\"date\", \"daily_hours\"])\n", | |
| "\n", | |
| " df = pd.DataFrame(data)\n", | |
| "\n", | |
| " # --- 2. DATA PROCESSING ---\n", | |
| " df[\"date\"] = pd.to_datetime(df[\"date\"]).dt.tz_localize(None).dt.normalize()\n", | |
| " df = df.groupby(\"date\")[\"daily_hours\"].sum().to_frame()\n", | |
| "\n", | |
| " # --- 3. FILL GAPS ---\n", | |
| " last_day_of_month = calendar.monthrange(now.year, now.month)[1]\n", | |
| " end_of_month_date = datetime(now.year, now.month, last_day_of_month)\n", | |
| "\n", | |
| " full_idx = pd.date_range(\n", | |
| " start=start_time.replace(tzinfo=None).date(),\n", | |
| " end=end_of_month_date.date(),\n", | |
| " freq='D'\n", | |
| " )\n", | |
| " df = df.reindex(full_idx, fill_value=0)\n", | |
| " df.index.name = 'date'\n", | |
| "\n", | |
| " # --- 4. CUMULATIVE MATH ---\n", | |
| " df[\"month_group\"] = df.index.to_period('M')\n", | |
| " df[\"monthly_cumulative\"] = df.groupby(\"month_group\")[\"daily_hours\"].cumsum()\n", | |
| "\n", | |
| " return df\n", | |
| "\n", | |
| "# --- OUTPUT GENERATION ---\n", | |
| "DAILY_COLOR = \"#298c8c\" # \"#4c78a8\"\n", | |
| "CUMULATIVE_COLOR = \"#800074\" # \"#ff7f0e\"\n", | |
| "\n", | |
| "if PROJECT_ID != \"YOUR_PROJECT_ID_HERE\":\n", | |
| " df = get_eecu_data(PROJECT_ID, MONTHS_TO_SHOW)\n", | |
| "\n", | |
| " if not df.empty and 'daily_hours' in df.columns:\n", | |
| "\n", | |
| " # --- STATUS CHECK ---\n", | |
| " current_date = pd.Timestamp.today().normalize()\n", | |
| " if current_date in df.index:\n", | |
| " current_val = df.loc[current_date, \"monthly_cumulative\"]\n", | |
| " else:\n", | |
| " current_val = df[\"monthly_cumulative\"].iloc[-1]\n", | |
| "\n", | |
| " if QUOTA_LIMIT:\n", | |
| " if current_val < QUOTA_LIMIT:\n", | |
| " remaining = QUOTA_LIMIT - current_val\n", | |
| " status_msg = f\"**✅ Within {SHOW_QUOTA_TIER} Tier** ({current_val:.1f} / {QUOTA_LIMIT} hrs used). You have **{remaining:.1f} hours** remaining.\"\n", | |
| " else:\n", | |
| " status_msg = f\"**⛔ Quota Depleted** ({current_val:.1f} hrs used). You have exceeded the {QUOTA_LIMIT}hr {SHOW_QUOTA_TIER} limit.\"\n", | |
| " else:\n", | |
| " status_msg = f\"**{current_val:.1f} hours** used this month.\"\n", | |
| "\n", | |
| " display(Markdown(f\"### Current Month Status: {status_msg}\"))\n", | |
| "\n", | |
| " # --- PLOTLY CHART ---\n", | |
| " fig = make_subplots(specs=[[{\"secondary_y\": True}]])\n", | |
| "\n", | |
| " # A. Bar chart (daily)\n", | |
| " fig.add_trace(\n", | |
| " go.Bar(\n", | |
| " x=df.index,\n", | |
| " y=df['daily_hours'],\n", | |
| " name=\"Daily Usage\",\n", | |
| " marker_color=DAILY_COLOR,\n", | |
| " opacity=0.7,\n", | |
| " hovertemplate='%{x|%b %d}<br>Daily: %{y:.2f} hrs<extra></extra>'\n", | |
| " ),\n", | |
| " secondary_y=False,\n", | |
| " )\n", | |
| "\n", | |
| " # B. Line chart (cumulative)\n", | |
| " fig.add_trace(\n", | |
| " go.Scatter(\n", | |
| " x=df.index,\n", | |
| " y=df['monthly_cumulative'],\n", | |
| " name=\"Month Total\",\n", | |
| " mode='lines',\n", | |
| " line=dict(color=CUMULATIVE_COLOR, width=3),\n", | |
| " hovertemplate='%{x|%b %d}<br>Month Total: %{y:.2f} hrs<extra></extra>'\n", | |
| " ),\n", | |
| " secondary_y=True,\n", | |
| " )\n", | |
| "\n", | |
| " # C. month separators\n", | |
| " month_starts = df.resample('MS').first().index\n", | |
| " for date in month_starts:\n", | |
| " fig.add_vline(x=date, line_width=1, line_dash=\"dash\", line_color=\"black\", opacity=0.3)\n", | |
| "\n", | |
| " # D. Quota guideline (based on selection)\n", | |
| " max_monthly = df['monthly_cumulative'].max()\n", | |
| " range_max = max_monthly * 1.1\n", | |
| "\n", | |
| " if QUOTA_LIMIT:\n", | |
| " fig.add_hline(\n", | |
| " y=QUOTA_LIMIT, line_dash=\"dot\", line_color=\"orange\", secondary_y=True,\n", | |
| " )\n", | |
| " # Ensure limit is visible if usage is low\n", | |
| " range_max = max(range_max, QUOTA_LIMIT * 1.05)\n", | |
| "\n", | |
| " # Layout\n", | |
| " fig.update_layout(\n", | |
| " title_text=f\"Earth Engine EECU Usage (Last {MONTHS_TO_SHOW} Months)\",\n", | |
| " hovermode=\"x unified\",\n", | |
| " showlegend=False,\n", | |
| " height=500,\n", | |
| " bargap=0.1,\n", | |
| " margin=dict(l=20, r=20, t=50, b=20),\n", | |
| " template=\"simple_white\"\n", | |
| " )\n", | |
| "\n", | |
| " max_daily = df['daily_hours'].max()\n", | |
| " if pd.isna(max_daily) or max_daily == 0: max_daily = 1\n", | |
| "\n", | |
| " fig.update_yaxes(title_text=\"Daily Usage (EECU-Hours)\", color=DAILY_COLOR, secondary_y=False, showgrid=False, range=[0, max_daily * 1.1])\n", | |
| " fig.update_yaxes(title_text=\"Cumulative Monthly Total (EECU-Hours)<br>(resets to zero on the first of each month)\", color=CUMULATIVE_COLOR, secondary_y=True, showgrid=False, range=[0, range_max])\n", | |
| "\n", | |
| " fig.show()\n", | |
| "\n", | |
| " # --- SUMMARY TABLE ---\n", | |
| " summary = df.resample('ME')['daily_hours'].agg(['sum', 'mean', 'max']).round(2)\n", | |
| " summary.columns = ['Total EECU-Hours', 'Avg Daily', 'Peak Day']\n", | |
| " summary.index = summary.index.strftime('%B %Y')\n", | |
| " summary = summary.reset_index().rename(columns={'date': 'Month'})\n", | |
| "\n", | |
| " # Display as a clean table\n", | |
| " print(\"\\n\")\n", | |
| " display(summary.style.format({\n", | |
| " 'Total EECU-Hours': '{:.2f}',\n", | |
| " 'Avg Daily': '{:.2f}',\n", | |
| " 'Peak Day': '{:.2f}'}).hide(axis='index'))\n", | |
| "\n", | |
| " else:\n", | |
| " print(\"No data found. Check your Project ID.\")\n", | |
| "else:\n", | |
| " print(\"Please enter a valid Project ID above.\")" | |
| ], | |
| "metadata": { | |
| "id": "4xMpkYkO2Z2C", | |
| "cellView": "form" | |
| }, | |
| "execution_count": null, | |
| "outputs": [] | |
| } | |
| ] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment