Adding gsm paging analysis
Browse files- Changelog.md +5 -0
- README.md +2 -0
- app.py +8 -2
- apps/kpi_analysis/gsm_lac_load.py +296 -0
- assets/gsm_lac_load.png +0 -0
Changelog.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1 |
|
2 |
# CHANGELOGS
|
3 |
|
|
|
|
|
|
|
|
|
|
|
4 |
## [0.2.8] - 2025-04-22
|
5 |
|
6 |
- upgrade streamlit version to 1.44
|
|
|
1 |
|
2 |
# CHANGELOGS
|
3 |
|
4 |
+
## [0.2.9] - 2025-06-16
|
5 |
+
|
6 |
+
- Add paging analysis App
|
7 |
+
- Add capacity analysis App
|
8 |
+
|
9 |
## [0.2.8] - 2025-04-22
|
10 |
|
11 |
- upgrade streamlit version to 1.44
|
README.md
CHANGED
@@ -50,6 +50,8 @@ You can access the hosted version of the app at [https://davmelchi-db-query.hf.s
|
|
50 |
- [x] Add the ability to select columns
|
51 |
- [x] Add authentication
|
52 |
- [x] Initial frequency distribution chart GSM
|
|
|
|
|
53 |
- [ ] Improve Dashboard
|
54 |
- [ ] Error handling
|
55 |
- [ ] Add KPI analysis App
|
|
|
50 |
- [x] Add the ability to select columns
|
51 |
- [x] Add authentication
|
52 |
- [x] Initial frequency distribution chart GSM
|
53 |
+
- [x] Add paging analysis App
|
54 |
+
- [x] Add capacity analysis App
|
55 |
- [ ] Improve Dashboard
|
56 |
- [ ] Error handling
|
57 |
- [ ] Add KPI analysis App
|
app.py
CHANGED
@@ -108,7 +108,7 @@ if check_password():
|
|
108 |
layout="wide",
|
109 |
initial_sidebar_state="expanded",
|
110 |
menu_items={
|
111 |
-
"About": "**📡 NPO DB Query v0.2.
|
112 |
},
|
113 |
)
|
114 |
|
@@ -133,7 +133,7 @@ if check_password():
|
|
133 |
"apps/import_physical_db.py", title="🌏Physical Database Verification"
|
134 |
),
|
135 |
],
|
136 |
-
"
|
137 |
st.Page(
|
138 |
"apps/kpi_analysis/gsm_capacity.py",
|
139 |
title=" 📊 GSM Capacity Analysis",
|
@@ -155,6 +155,12 @@ if check_password():
|
|
155 |
title=" 📊 LTE Capacity Analysis",
|
156 |
),
|
157 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
"Documentations": [
|
159 |
st.Page(
|
160 |
"documentations/database_doc.py", title="📚Databases Documentation"
|
|
|
108 |
layout="wide",
|
109 |
initial_sidebar_state="expanded",
|
110 |
menu_items={
|
111 |
+
"About": "**📡 NPO DB Query v0.2.9**",
|
112 |
},
|
113 |
)
|
114 |
|
|
|
133 |
"apps/import_physical_db.py", title="🌏Physical Database Verification"
|
134 |
),
|
135 |
],
|
136 |
+
"Capacity Analysis": [
|
137 |
st.Page(
|
138 |
"apps/kpi_analysis/gsm_capacity.py",
|
139 |
title=" 📊 GSM Capacity Analysis",
|
|
|
155 |
title=" 📊 LTE Capacity Analysis",
|
156 |
),
|
157 |
],
|
158 |
+
"Paging Analysis": [
|
159 |
+
st.Page(
|
160 |
+
"apps/kpi_analysis/gsm_lac_load.py",
|
161 |
+
title=" 📊 GSM LAC Load Analysis",
|
162 |
+
),
|
163 |
+
],
|
164 |
"Documentations": [
|
165 |
st.Page(
|
166 |
"documentations/database_doc.py", title="📚Databases Documentation"
|
apps/kpi_analysis/gsm_lac_load.py
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Tuple
|
2 |
+
|
3 |
+
import pandas as pd
|
4 |
+
import plotly.express as px
|
5 |
+
import streamlit as st
|
6 |
+
|
7 |
+
from queries.process_gsm import combined_gsm_database
|
8 |
+
from utils.convert_to_excel import convert_gsm_dfs, save_dataframe
|
9 |
+
from utils.kpi_analysis_utils import create_hourly_date, kpi_naming_cleaning
|
10 |
+
|
11 |
+
# Constants
|
12 |
+
GSM_COLUMNS = [
|
13 |
+
"ID_BTS",
|
14 |
+
"BSC",
|
15 |
+
"code",
|
16 |
+
"Region",
|
17 |
+
"locationAreaIdLAC",
|
18 |
+
"Longitude",
|
19 |
+
"Latitude",
|
20 |
+
]
|
21 |
+
|
22 |
+
TRX_COLUMNS = [
|
23 |
+
"ID_BTS",
|
24 |
+
"number_trx_per_cell",
|
25 |
+
"number_tch_per_cell",
|
26 |
+
"number_sd_per_cell",
|
27 |
+
"number_bcch_per_cell",
|
28 |
+
"number_ccch_per_cell",
|
29 |
+
"number_cbc_per_cell",
|
30 |
+
"number_total_channels_per_cell",
|
31 |
+
"number_signals_per_cell",
|
32 |
+
]
|
33 |
+
|
34 |
+
KPI_COLUMNS = [
|
35 |
+
"BSC_name",
|
36 |
+
"BCF_name",
|
37 |
+
"BTS_name",
|
38 |
+
"Paging_messages_on_air_interface",
|
39 |
+
"DELETE_PAGING_COMMAND_c003038",
|
40 |
+
"datetime",
|
41 |
+
"date",
|
42 |
+
"hour",
|
43 |
+
]
|
44 |
+
|
45 |
+
|
46 |
+
def get_gsm_databases(dump_path: str) -> pd.DataFrame:
|
47 |
+
"""
|
48 |
+
Process GSM database dump and return combined DataFrame with BTS and TRX data.
|
49 |
+
|
50 |
+
Args:
|
51 |
+
dump_path: Path to the GSM dump file
|
52 |
+
|
53 |
+
Returns:
|
54 |
+
pd.DataFrame: Combined DataFrame with BTS and TRX information
|
55 |
+
"""
|
56 |
+
dfs = combined_gsm_database(dump_path)
|
57 |
+
bts_df: pd.DataFrame = dfs[0]
|
58 |
+
trx_df: pd.DataFrame = dfs[2]
|
59 |
+
|
60 |
+
# Clean GSM df
|
61 |
+
bts_df = bts_df[GSM_COLUMNS]
|
62 |
+
trx_df = trx_df[TRX_COLUMNS]
|
63 |
+
trx_df = trx_df.drop_duplicates(subset=["ID_BTS"])
|
64 |
+
|
65 |
+
gsm_df = pd.merge(bts_df, trx_df, on="ID_BTS", how="left")
|
66 |
+
|
67 |
+
# Create BSC_Lac column
|
68 |
+
gsm_df["BSC_Lac"] = (
|
69 |
+
gsm_df["BSC"].astype(str) + "_" + gsm_df["locationAreaIdLAC"].astype(str)
|
70 |
+
)
|
71 |
+
|
72 |
+
# Calculate number of TRX per LAC
|
73 |
+
gsm_df["number_trx_per_lac"] = gsm_df.groupby("BSC_Lac")[
|
74 |
+
"number_trx_per_cell"
|
75 |
+
].transform("sum")
|
76 |
+
|
77 |
+
return gsm_df
|
78 |
+
|
79 |
+
|
80 |
+
def analyze_lac_load_kpi(hourly_report_path: str) -> pd.DataFrame:
|
81 |
+
"""
|
82 |
+
Process hourly KPI report and prepare it for LAC load analysis.
|
83 |
+
|
84 |
+
Args:
|
85 |
+
hourly_report_path: Path to the hourly KPI report CSV file
|
86 |
+
|
87 |
+
Returns:
|
88 |
+
pd.DataFrame: Processed DataFrame with KPI data
|
89 |
+
"""
|
90 |
+
df = pd.read_csv(hourly_report_path, delimiter=";")
|
91 |
+
df = kpi_naming_cleaning(df)
|
92 |
+
df = create_hourly_date(df)
|
93 |
+
df = df[KPI_COLUMNS]
|
94 |
+
|
95 |
+
# Clean and process BTS codes
|
96 |
+
df = df[df["BTS_name"].str.len() >= 5]
|
97 |
+
df["code"] = df["BTS_name"].str.split("_").str[0]
|
98 |
+
df["code"] = pd.to_numeric(df["code"], errors="coerce").fillna(0).astype(int)
|
99 |
+
|
100 |
+
return df
|
101 |
+
|
102 |
+
|
103 |
+
def analyze_lac_load(dump_path: str, hourly_report_path: str) -> List[pd.DataFrame]:
|
104 |
+
"""
|
105 |
+
Analyze LAC load from GSM dump and hourly KPI report.
|
106 |
+
|
107 |
+
Args:
|
108 |
+
dump_path: Path to the GSM dump file
|
109 |
+
hourly_report_path: Path to the hourly KPI report CSV file
|
110 |
+
|
111 |
+
Returns:
|
112 |
+
List containing two DataFrames: [lac_load_df, max_paging_df]
|
113 |
+
"""
|
114 |
+
gsm_df = get_gsm_databases(dump_path)
|
115 |
+
lac_load_df = analyze_lac_load_kpi(hourly_report_path)
|
116 |
+
lac_load_df = pd.merge(gsm_df, lac_load_df, on="code", how="left")
|
117 |
+
|
118 |
+
# Aggregate data
|
119 |
+
lac_load_df = (
|
120 |
+
lac_load_df.groupby(
|
121 |
+
[
|
122 |
+
"datetime",
|
123 |
+
"date",
|
124 |
+
"hour",
|
125 |
+
"BSC_name",
|
126 |
+
"BSC_Lac",
|
127 |
+
"number_trx_per_lac",
|
128 |
+
]
|
129 |
+
)
|
130 |
+
.agg(
|
131 |
+
{
|
132 |
+
"Paging_messages_on_air_interface": "max",
|
133 |
+
"DELETE_PAGING_COMMAND_c003038": "max",
|
134 |
+
}
|
135 |
+
)
|
136 |
+
.reset_index()
|
137 |
+
)
|
138 |
+
|
139 |
+
# Get max paging messages
|
140 |
+
max_paging_messages = lac_load_df.sort_values(
|
141 |
+
by=["BSC_Lac", "Paging_messages_on_air_interface"], ascending=False
|
142 |
+
).drop_duplicates(subset=["BSC_Lac"], keep="first")[
|
143 |
+
[
|
144 |
+
"BSC_name",
|
145 |
+
"BSC_Lac",
|
146 |
+
"number_trx_per_lac",
|
147 |
+
"Paging_messages_on_air_interface",
|
148 |
+
]
|
149 |
+
]
|
150 |
+
|
151 |
+
# Get max delete paging commands
|
152 |
+
max_delete_paging = lac_load_df.sort_values(
|
153 |
+
by=["BSC_Lac", "DELETE_PAGING_COMMAND_c003038"], ascending=False
|
154 |
+
).drop_duplicates(subset=["BSC_Lac"], keep="first")[
|
155 |
+
["BSC_name", "BSC_Lac", "DELETE_PAGING_COMMAND_c003038"]
|
156 |
+
]
|
157 |
+
|
158 |
+
# Merge results
|
159 |
+
max_paging_df = pd.merge(
|
160 |
+
max_paging_messages,
|
161 |
+
max_delete_paging,
|
162 |
+
on=["BSC_name", "BSC_Lac"],
|
163 |
+
how="left",
|
164 |
+
)
|
165 |
+
|
166 |
+
# Calculate utilization (paging/640800)
|
167 |
+
max_paging_df["Utilization"] = (
|
168 |
+
(max_paging_df["Paging_messages_on_air_interface"] / 640800) * 100
|
169 |
+
).round(2)
|
170 |
+
|
171 |
+
return [lac_load_df, max_paging_df]
|
172 |
+
|
173 |
+
|
174 |
+
def display_ui() -> None:
|
175 |
+
"""Display the Streamlit user interface."""
|
176 |
+
st.title(" 📊 GSM LAC Load Analysis")
|
177 |
+
doc_col, image_col = st.columns(2)
|
178 |
+
|
179 |
+
with doc_col:
|
180 |
+
st.write(
|
181 |
+
"""
|
182 |
+
The report should be run with a minimum of 7 days of data.
|
183 |
+
- Dump file required
|
184 |
+
- Hourly Report in CSV format
|
185 |
+
"""
|
186 |
+
)
|
187 |
+
|
188 |
+
with image_col:
|
189 |
+
st.image("./assets/gsm_lac_load.png", width=250)
|
190 |
+
|
191 |
+
|
192 |
+
@st.fragment
|
193 |
+
def display_filtered_lac_load(lac_load_df: pd.DataFrame) -> None:
|
194 |
+
"""
|
195 |
+
Display filtered LAC load data with interactive charts.
|
196 |
+
|
197 |
+
Args:
|
198 |
+
lac_load_df: DataFrame containing LAC load data
|
199 |
+
"""
|
200 |
+
st.write("### Filtered LAC Load by BSC and BSC_Lac")
|
201 |
+
|
202 |
+
bsc_col, bsc_lac_col = st.columns(2)
|
203 |
+
|
204 |
+
with bsc_col:
|
205 |
+
selected_bsc = st.multiselect(
|
206 |
+
"Select BSC",
|
207 |
+
lac_load_df["BSC_name"].unique(),
|
208 |
+
key="selected_bsc",
|
209 |
+
default=[lac_load_df["BSC_name"].unique()[0]],
|
210 |
+
)
|
211 |
+
|
212 |
+
with bsc_lac_col:
|
213 |
+
selected_bsc_lac = st.multiselect(
|
214 |
+
"Select BSC_Lac",
|
215 |
+
lac_load_df[lac_load_df["BSC_name"].isin(selected_bsc)]["BSC_Lac"].unique(),
|
216 |
+
key="selected_bsc_lac",
|
217 |
+
default=lac_load_df[lac_load_df["BSC_name"].isin(selected_bsc)][
|
218 |
+
"BSC_Lac"
|
219 |
+
].unique(),
|
220 |
+
)
|
221 |
+
|
222 |
+
filtered_lac_load_df = lac_load_df[
|
223 |
+
lac_load_df["BSC_name"].isin(selected_bsc)
|
224 |
+
& lac_load_df["BSC_Lac"].isin(selected_bsc_lac)
|
225 |
+
]
|
226 |
+
|
227 |
+
# Display charts
|
228 |
+
chart1, chart2 = st.columns(2)
|
229 |
+
with chart1:
|
230 |
+
st.write("### Paging Messages on Air Interface")
|
231 |
+
fig1 = px.line(
|
232 |
+
filtered_lac_load_df,
|
233 |
+
x="datetime",
|
234 |
+
y="Paging_messages_on_air_interface",
|
235 |
+
color="BSC_Lac",
|
236 |
+
title="Max Paging Messages on Air Interface Per BSC_Lac",
|
237 |
+
)
|
238 |
+
fig1.update_layout(
|
239 |
+
xaxis_title="<b>Datetime</b>",
|
240 |
+
yaxis_title="<b>Paging Messages on Air Interface</b>",
|
241 |
+
)
|
242 |
+
fig1.add_hline(y=256000, line_color="red", line_dash="dash", line_width=2)
|
243 |
+
st.plotly_chart(fig1)
|
244 |
+
|
245 |
+
with chart2:
|
246 |
+
st.write("### Delete Paging Commands")
|
247 |
+
fig2 = px.line(
|
248 |
+
filtered_lac_load_df,
|
249 |
+
x="datetime",
|
250 |
+
y="DELETE_PAGING_COMMAND_c003038",
|
251 |
+
color="BSC_Lac",
|
252 |
+
title="Max Delete Paging Commands Per BSC_Lac",
|
253 |
+
)
|
254 |
+
fig2.update_layout(
|
255 |
+
xaxis_title="<b>Datetime</b>",
|
256 |
+
yaxis_title="<b>Delete Paging Commands</b>",
|
257 |
+
)
|
258 |
+
st.plotly_chart(fig2)
|
259 |
+
|
260 |
+
st.write("### Filtered LAC Load Data")
|
261 |
+
st.dataframe(filtered_lac_load_df)
|
262 |
+
|
263 |
+
|
264 |
+
def main() -> None:
|
265 |
+
"""Main function to run the Streamlit app."""
|
266 |
+
display_ui()
|
267 |
+
|
268 |
+
# File uploaders
|
269 |
+
file1, file2 = st.columns(2)
|
270 |
+
with file1:
|
271 |
+
uploaded_dump = st.file_uploader("Upload Dump file in xlsb format", type="xlsb")
|
272 |
+
with file2:
|
273 |
+
uploaded_hourly_report = st.file_uploader(
|
274 |
+
"Upload Hourly Report in CSV format", type="csv"
|
275 |
+
)
|
276 |
+
|
277 |
+
if uploaded_dump is not None and uploaded_hourly_report is not None:
|
278 |
+
if st.button("Analyze Data", type="primary"):
|
279 |
+
with st.spinner("Analyzing data..."):
|
280 |
+
dfs = analyze_lac_load(
|
281 |
+
dump_path=uploaded_dump,
|
282 |
+
hourly_report_path=uploaded_hourly_report,
|
283 |
+
)
|
284 |
+
|
285 |
+
lac_load_df = dfs[0]
|
286 |
+
max_paging_df = dfs[1]
|
287 |
+
|
288 |
+
if lac_load_df is not None and "lac_load_df" not in st.session_state:
|
289 |
+
st.session_state.lac_load_df = lac_load_df
|
290 |
+
st.write("### LAC Load and Utilization with Max Paging 640800")
|
291 |
+
st.dataframe(max_paging_df)
|
292 |
+
display_filtered_lac_load(lac_load_df)
|
293 |
+
|
294 |
+
|
295 |
+
if __name__ == "__main__":
|
296 |
+
main()
|
assets/gsm_lac_load.png
ADDED
![]() |