Analyze UNS Data
Available inCloudEnterprise
Tier0 adopts Marimo Notebook for advanced data analysis with Python.
Creating Notebooks
Section titled “Creating Notebooks”- Log in to Tier0, and select Notebook.
- Hover over the +, click New Notebook.
- Enter the notebook Name and click Create.
- Click and enter the created notebook to start editing.
Querying Data from UNS
Section titled “Querying Data from UNS”-
Inside the notebook, click the VARIABLES on the left side.
-
Expand the PostgreSQL list, select a table under tenant_id > uns.
-
Hover over the table, and click the + icon next to the table name to generate SQL statements cell automatically.
Terminal window SELECT * FROM "uns"."table_name" LIMIT 100 -
Click the Run icon at the lower-right corner to run all cells and get the query result.
Analyzing UNS Data
Section titled “Analyzing UNS Data”Example Background
Section titled “Example Background”- Data source is temperature, pressure, status and vibration of a compressor collected from a factory and sent to Tier0 UNS.
- Isolation Forest is used to recognize abnormal data points in the dataset.
- Detection sensitivity can be adjusted, and the results are visualized accordingly in a dashboard.
Operations
Section titled “Operations”- Create a notebook and access it.
- Add the following cells in order.
Cell 1: Import dependencies.
import marimo as moimport pandas as pdimport numpy as npimport altair as alt
from sklearn.preprocessing import StandardScalerfrom sklearn.ensemble import IsolationForestCell 2: Get data from UNS, clean it to get the analysis dataset.
status_pd = status_data.to_pandas()temperature_pd = temperature_data.to_pandas()pressure_pd = pressure_data.to_pandas()vibration_pd = vibration_data.to_pandas()
# Convert and sort timestampsfor frame in [ status_pd, temperature_pd, pressure_pd, vibration_pd,]: frame["_timestamp"] = pd.to_datetime( frame["_timestamp"], utc=True, errors="coerce", )
frame.dropna( subset=["_timestamp"], inplace=True, )
frame.sort_values( "_timestamp", inplace=True, )
# Use temperature as the main timelinesensor_data = pd.merge_asof( temperature_pd, pressure_pd, on="_timestamp", direction="nearest", tolerance=pd.Timedelta(seconds=30),)
sensor_data = pd.merge_asof( sensor_data, vibration_pd, on="_timestamp", direction="nearest", tolerance=pd.Timedelta(seconds=30),)
sensor_data = pd.merge_asof( sensor_data, status_pd, on="_timestamp", direction="nearest", tolerance=pd.Timedelta(seconds=30),)
# Remove incomplete recordscomplete_data = sensor_data.dropna( subset=[ "temperature", "pressure", "vibration", "status_code", ])
# Analyze only active running periodsrunning_data = ( complete_data[ complete_data["status_code"] == 1 ] .reset_index(drop=True))
running_dataCell 3: Set the slider for anomaly detection threshold.
anomaly_threshold = mo.ui.slider( start=-0.15, stop=0.10, step=0.01, value=0.00, label="Anomaly detection threshold",)
mo.vstack([ mo.md("## Model Settings"), mo.md( """Adjust the anomaly detection threshold.
A higher threshold applies stricter criteria and identifies more operating records as anomalies. """ ), anomaly_threshold,])Cell 4: Use the Isolation Forest model to detect anomalies.
feature_names = [ "temperature", "pressure", "vibration",]
# Prepare model inputmodel_input = running_data[ feature_names]
# Standardize sensor valuesfeature_scaler = StandardScaler()
scaled_features = feature_scaler.fit_transform( model_input)
# Train the anomaly detection modelanomaly_model = IsolationForest( n_estimators=200, contamination="auto", random_state=42,)
anomaly_model.fit( scaled_features)
# Calculate continuous anomaly scoresanomaly_scores = anomaly_model.decision_function( scaled_features)
# Prepare analysis resultsml_result = running_data.copy()
ml_result["anomaly_score"] = anomaly_scores
# Classify records using the interactive thresholdml_result["condition"] = np.where( ml_result["anomaly_score"] < anomaly_threshold.value, "Anomaly", "Normal",)
ml_resultCell 5: Displays results on a dashboard.
# Calculate summary metricsdashboard_total = len(ml_result)
dashboard_normal = int( ( ml_result["condition"] == "Normal" ).sum())
dashboard_anomaly = int( ( ml_result["condition"] == "Anomaly" ).sum())
dashboard_anomaly_ratio = ( dashboard_anomaly / dashboard_total * 100 if dashboard_total > 0 else 0)
# Calculate anomaly severityanomaly_records = ml_result[ ml_result["condition"] == "Anomaly"]
if len(anomaly_records) > 0: average_anomaly_severity = float( ( anomaly_threshold.value - anomaly_records["anomaly_score"] ) .clip(lower=0) .mean() )else: average_anomaly_severity = 0.0
# Illustrative equipment health scoreseverity_penalty = min( average_anomaly_severity * 80, 20)
dashboard_health_score = max( 0, 100 - dashboard_anomaly_ratio * 1.2 - severity_penalty)
# Determine risk levelif dashboard_health_score < 60: dashboard_risk = "High" dashboard_conclusion = ( "The compressor shows a high level of unusual operating behavior. " "Further equipment inspection is recommended." )
elif dashboard_health_score < 80: dashboard_risk = "Medium" dashboard_conclusion = ( "Some unusual operating patterns were detected. " "Continue monitoring the equipment and review the abnormal periods." )
else: dashboard_risk = "Low" dashboard_conclusion = ( "Most operating records follow the learned normal pattern. " "No significant abnormal behavior was detected." )
# Headerdashboard_header = mo.Html( """ <div style=" padding: 4px 0 18px 0; font-family: Arial, sans-serif; "> <div style=" font-size: 26px; font-weight: 700; color: #1f2937; margin-bottom: 6px; "> Air Compressor Anomaly Detection </div>
<div style=" font-size: 14px; color: #6b7280; line-height: 1.5; "> Machine learning analysis based on temperature, pressure, and vibration during active running periods. </div> </div> """)# Conclusion carddashboard_conclusion_card = mo.Html( f""" <div style=" background: #f8fafc; border: 1px solid #e5e7eb; border-left: 4px solid #4f46e5; border-radius: 8px; padding: 16px 18px; margin-bottom: 18px; font-family: Arial, sans-serif; "> <div style=" font-size: 12px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 6px; "> Analysis Summary </div>
<div style=" font-size: 15px; color: #1f2937; line-height: 1.6; "> {dashboard_conclusion} </div> </div> """)# KPI cardsdashboard_kpis = mo.Html( f""" <div style=" display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 24px; font-family: Arial, sans-serif; ">
<div style=" border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 18px; background: white; "> <div style=" font-size: 12px; color: #6b7280; margin-bottom: 8px; "> HEALTH SCORE </div>
<div style=" font-size: 28px; font-weight: 700; color: #111827; "> {dashboard_health_score:.1f} <span style=" font-size: 14px; color: #9ca3af; "> / 100 </span> </div> </div>
<div style=" border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 18px; background: white; "> <div style=" font-size: 12px; color: #6b7280; margin-bottom: 8px; "> RISK LEVEL </div>
<div style=" font-size: 28px; font-weight: 700; color: #111827; "> {dashboard_risk} </div> </div>
<div style=" border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 18px; background: white; "> <div style=" font-size: 12px; color: #6b7280; margin-bottom: 8px; "> ANALYZED SAMPLES </div>
<div style=" font-size: 28px; font-weight: 700; color: #111827; "> {dashboard_total:,} </div> </div>
<div style=" border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 18px; background: white; "> <div style=" font-size: 12px; color: #6b7280; margin-bottom: 8px; "> ANOMALY RATIO </div>
<div style=" font-size: 28px; font-weight: 700; color: #111827; "> {dashboard_anomaly_ratio:.1f}% </div> </div>
</div> """)
# Interactive anomaly chartdashboard_chart = ( alt.Chart(ml_result) .mark_circle( size=58, opacity=0.78, ) .encode( x=alt.X( "_timestamp:T", title="Time", ),
y=alt.Y( "vibration:Q", title="Vibration RMS (mm/s)", ),
color=alt.Color( "condition:N", title=None, scale=alt.Scale( domain=[ "Normal", "Anomaly", ], range=[ "#94a3b8", "#ef4444", ], ), ),
tooltip=[ alt.Tooltip( "_timestamp:T", title="Time", ),
alt.Tooltip( "temperature:Q", title="Temperature", format=".2f", ),
alt.Tooltip( "pressure:Q", title="Pressure", format=".4f", ),
alt.Tooltip( "vibration:Q", title="Vibration", format=".3f", ),
alt.Tooltip( "anomaly_score:Q", title="Anomaly Score", format=".3f", ),
alt.Tooltip( "condition:N", title="Condition", ), ], ) .properties( width=980, height=380,
title=alt.TitleParams( text="Detected Operating Anomalies", subtitle=( f"Threshold: {anomaly_threshold.value:.2f} · " f"{dashboard_anomaly:,} anomalies detected" ), anchor="start", fontSize=17, fontWeight=600, subtitleFontSize=12, subtitleColor="#6b7280", offset=14, ), ) .configure_view( stroke=None ) .interactive())
# Final dashboardmo.vstack([ dashboard_header, dashboard_conclusion_card, dashboard_kpis, mo.ui.altair_chart( dashboard_chart ),])- Click Run all stale cells at the lower-right corner to run all cells and view the results.
- Build Apps on UNS — Build industrial applications with UNS data.
- UNS Concepts — Understand the data model you just analyzed.