Telecom — network performance & capacity
The problem. Subscribers notice a degraded cell long before a threshold report does — dropped calls, stalling video, dead zones at rush hour. The network operations centre needs to spot a degrading or congesting cell and localize the cause, then see far enough ahead to add capacity before the busy hour turns into an outage. The catch: a cell's problem is often not in the cell at all, but in a backhaul link it shares with others.
What we solve here is finding and localizing degradation fast, and planning capacity before congestion bites.
Set up demo data
New workspace? Run this once (Python) to create two cells' utilisation series (with busy-hour peaks) and the topology where they share a backhaul link — so the localisation in step 2 points at the link. Safe to re-run.
import datahub_sdk, numpy as np, pandas as pd
client = datahub_sdk.DataHubClient.from_env()
util = 50 + 40 * np.clip(np.sin(np.arange(168) / 24 * 2 * np.pi), 0, 1) # ~90% at busy hour
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=168, freq="1h")
for c in ["cell_oslo_4412", "cell_oslo_4418"]:
client.timeseries.create([datahub_sdk.TimeSeries(external_id=f"{c}_prb_util", name=f"{c} PRB util", unit="pct", value_type="float")])
client.timeseries.insert_from_lists(timestamps=idx, values=util, ts=f"{c}_prb_util")
client.resources.create(
[datahub_sdk.Resource(external_id=x, name=x, labels=[lbl]) for x, lbl in
[("cell_oslo_4412", "Cell"), ("cell_oslo_4418", "Cell"),
("backhaul_link_88", "BackhaulLink"), ("controller_rnc_3", "Controller")]],
[datahub_sdk.RelForm.by_external_ids("cell_oslo_4412", "backhaul_link_88", "backhauled_by"),
datahub_sdk.RelForm.by_external_ids("cell_oslo_4418", "backhaul_link_88", "backhauled_by"),
datahub_sdk.RelForm.by_external_ids("backhaul_link_88", "controller_rnc_3", "connects_to")])
1. Find the busy-hour congestion
Per-cell throughput, drop rate and active users are series. Aggregating to hourly peaks reveals which cells run hot at the busy hour — the capacity-planning shortlist. See Query & aggregate.
- Java
- Python
- Rust
var filter = new RetrieveFilter();
filter.setExternalId("cell_oslo_4412_prb_util");
filter.setStart(ZonedDateTime.now().minusDays(7));
filter.setEnd(ZonedDateTime.now());
filter.setAggregates(List.of("max"));
filter.setGranularity("1h");
var request = new DataRetriever<RetrieveFilter>();
request.setItems(List.of(filter));
client.timeseries().retrieve(request).getItems().get(0).getDatapoints()
.forEach(p -> flagIfSaturated(p.getTimestamp(), p.getValue())); // PRB utilisation %
import pandas as pd
rf = datahub_sdk.RetrieveFilter(
ts="cell_oslo_4412_prb_util",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(days=7),
end=pd.Timestamp.now(tz="UTC"),
aggregates=["max"], granularity="1h")
for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints():
flag_if_saturated(dp.timestamp, dp.max)
use dataplatform_rust_sdk::generic::{DataWrapper, RetrieveFilter};
use chrono::Utc;
let filter = RetrieveFilter {
external_id: Some("cell_oslo_4412_prb_util".into()),
aggregates: Some(vec!["max".into()]),
granularity: Some("1h".into()),
start: Some(Utc::now() - chrono::Duration::days(7)),
end: Some(Utc::now()),
..Default::default()
};
let series = api.time_series
.retrieve_datapoints(&DataWrapper::from(vec![filter])).await?
.get_items().remove(0);
for p in &series.datapoints { flag_if_saturated(&p.timestamp, p.max); }
2. Localize a fault to shared backhaul
When several cells degrade together, the common cause is usually a backhaul link or controller they share. Model cells → backhaul → controller as a graph and walk from each degraded cell to the element they have in common — the shared-dependency pattern, pointing the field team at the one link to fix instead of a dozen cells to chase.
- Java
- Python
- Rust
ResourceNetwork c1 = client.resources().fetchRelated("cell_oslo_4412", 4);
ResourceNetwork c2 = client.resources().fetchRelated("cell_oslo_4418", 4);
Set<String> n1 = c1.nodes().stream().map(Resource::getExternalId).collect(Collectors.toSet());
Set<String> shared = c2.nodes().stream().map(Resource::getExternalId)
.filter(n1::contains).collect(Collectors.toSet());
// shared contains "backhaul_link_88" → fix the link, not the cells
c1 = client.resources.fetch_related(external_id="cell_oslo_4412", depth=4)
c2 = client.resources.fetch_related(external_id="cell_oslo_4418", depth=4)
shared = {n.external_id for n in c1.nodes} & {n.external_id for n in c2.nodes}
# 'backhaul_link_88' in shared
use dataplatform_rust_sdk::resources::RelatedResourcesForm;
use std::collections::HashSet;
let c1 = api.resources.fetch_related(
&RelatedResourcesForm::from_external_id("cell_oslo_4412").with_depth(4)).await?;
let c2 = api.resources.fetch_related(
&RelatedResourcesForm::from_external_id("cell_oslo_4418").with_depth(4)).await?;
let n1: HashSet<&str> = c1.nodes().iter().map(|n| n.external_id.as_str()).collect();
let shared: Vec<&str> = c2.nodes().iter()
.map(|n| n.external_id.as_str()).filter(|id| n1.contains(id)).collect();
// shared contains "backhaul_link_88"
See the result
Busy-hour utilisation peaks near 90%, and the two degraded cells share one element:
backhaul_link_88 ← fix the link, not the dozen cells behind it
See also
- Query & aggregate — busy-hour capacity planning.
- Correlate alarms with the graph — localize to shared backhaul.
- Turn readings into events — degradation alerts.