DavMelchi commited on
Commit
027f03b
ยท
1 Parent(s): 81c766f

LTE capacity 1st commit

Browse files
app.py CHANGED
@@ -142,6 +142,10 @@ if check_password():
142
  "apps/kpi_analysis/gsm_capacity.py",
143
  title=" ๐Ÿ“Š GSM Capacity Analysis",
144
  ),
 
 
 
 
145
  ],
146
  "Documentations": [
147
  st.Page(
@@ -154,6 +158,10 @@ if check_password():
154
  "documentations/gsm_capacity_docs.py",
155
  title="๐Ÿ“˜GSM Capacity Documentation",
156
  ),
 
 
 
 
157
  ],
158
  }
159
 
 
142
  "apps/kpi_analysis/gsm_capacity.py",
143
  title=" ๐Ÿ“Š GSM Capacity Analysis",
144
  ),
145
+ st.Page(
146
+ "apps/kpi_analysis/lte_capacity.py",
147
+ title=" ๐Ÿ“Š LTE Capacity Analysis",
148
+ ),
149
  ],
150
  "Documentations": [
151
  st.Page(
 
158
  "documentations/gsm_capacity_docs.py",
159
  title="๐Ÿ“˜GSM Capacity Documentation",
160
  ),
161
+ st.Page(
162
+ "documentations/lte_capacity_docs.py",
163
+ title="๐Ÿ“˜LTE Capacity Documentation",
164
+ ),
165
  ],
166
  }
167
 
apps/kpi_analysis/gsm_capacity.py CHANGED
@@ -2,11 +2,11 @@ import pandas as pd
2
  import plotly.express as px
3
  import streamlit as st
4
 
5
- from process_kpi.process_gsm_capacity import GsmCapacity, analyze_gsm_data
6
  from utils.convert_to_excel import ( # Import convert_dfs from the appropriate module
7
- convert_dfs,
8
  convert_gsm_dfs,
9
  )
 
10
 
11
  st.title(" ๐Ÿ“Š GSM Capacity Analysis")
12
  doc_col, image_col = st.columns(2)
@@ -241,3 +241,26 @@ if (
241
  textposition="outside",
242
  )
243
  st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import plotly.express as px
3
  import streamlit as st
4
 
5
+ from process_kpi.process_gsm_capacity import analyze_gsm_data
6
  from utils.convert_to_excel import ( # Import convert_dfs from the appropriate module
 
7
  convert_gsm_dfs,
8
  )
9
+ from utils.kpi_analysis_utils import GsmCapacity
10
 
11
  st.title(" ๐Ÿ“Š GSM Capacity Analysis")
12
  doc_col, image_col = st.columns(2)
 
241
  textposition="outside",
242
  )
243
  st.plotly_chart(fig, use_container_width=True)
244
+
245
+ # create a map plotly with gsm_analysis_df and max_tch_call_blocking_bh
246
+ st.markdown("***")
247
+ st.markdown(":blue[**Max TCH Call Blocking BH distribution**]")
248
+ fig = px.scatter_mapbox(
249
+ gsm_analysis_df.dropna(
250
+ subset=["max_tch_call_blocking_bh", "Latitude", "Longitude"]
251
+ ),
252
+ lat="Latitude",
253
+ lon="Longitude",
254
+ color=[
255
+ "red" if val > tch_blocking_threshold else "green"
256
+ for val in gsm_analysis_df[
257
+ "max_tch_call_blocking_bh"
258
+ ].dropna() # .values
259
+ ],
260
+ size="max_tch_call_blocking_bh",
261
+ zoom=10,
262
+ height=600,
263
+ title="Max TCH Call Blocking BH distribution",
264
+ )
265
+ fig.update_layout(mapbox_style="open-street-map")
266
+ st.plotly_chart(fig, use_container_width=True)
apps/kpi_analysis/lte_capacity.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import plotly.express as px
3
+ import streamlit as st
4
+
5
+ from process_kpi.process_lte_capacity import process_lte_bh_report
6
+ from utils.convert_to_excel import convert_lte_analysis_dfs
7
+ from utils.kpi_analysis_utils import LteCapacity
8
+
9
+ st.title("๐Ÿ“Š LTE Capacity Analysis")
10
+ doc_col, image_col = st.columns(2)
11
+
12
+ with doc_col:
13
+ st.write(
14
+ """
15
+ The report analyzes LTE capacity based on:
16
+ - Dump file required
17
+ - BH Cell level KPI report in CSV format
18
+ - Availability and PRB usage thresholds
19
+ """
20
+ )
21
+
22
+ with image_col:
23
+ st.image("./assets/lte_capacity.png", width=250)
24
+
25
+ file1, file2 = st.columns(2)
26
+
27
+ with file1:
28
+ uploaded_dump = st.file_uploader("Upload Dump file in xlsb format", type="xlsb")
29
+ with file2:
30
+ uploaded_bh_report = st.file_uploader(
31
+ "Upload LTE Busy Hour Report in CSV format", type="csv"
32
+ )
33
+
34
+ # Parameters
35
+ param_col1, param_col2 = st.columns(2)
36
+ param_col3, param_col4 = st.columns(2)
37
+
38
+ with param_col1:
39
+ num_last_days = st.number_input(
40
+ "Number of last days for analysis", value=7, min_value=1
41
+ )
42
+
43
+ with param_col2:
44
+ num_threshold_days = st.number_input(
45
+ "Number of days for threshold", value=3, min_value=1
46
+ )
47
+
48
+ with param_col3:
49
+ availability_threshold = st.number_input(
50
+ "Availability threshold (%)", value=95.0, min_value=0.0, max_value=100.0
51
+ )
52
+
53
+ with param_col4:
54
+ prb_usage_threshold = st.number_input(
55
+ "PRB usage threshold (%)", value=80.0, min_value=0.0, max_value=100.0
56
+ )
57
+
58
+ prb_diff_between_cells = st.number_input(
59
+ "Maximum PRB usage difference between cells (%)",
60
+ value=20.0,
61
+ min_value=0.0,
62
+ max_value=100.0,
63
+ )
64
+
65
+ if uploaded_dump is not None and uploaded_bh_report is not None:
66
+ if st.button("Analyze Data", type="primary"):
67
+ with st.spinner("Processing data..."):
68
+ results = process_lte_bh_report(
69
+ dump_path=uploaded_dump,
70
+ bh_report_path=uploaded_bh_report,
71
+ num_last_days=num_last_days,
72
+ num_threshold_days=num_threshold_days,
73
+ availability_threshold=availability_threshold,
74
+ prb_usage_threshold=prb_usage_threshold,
75
+ prb_diff_between_cells_threshold=prb_diff_between_cells,
76
+ )
77
+ if results is not None:
78
+ bh_report: pd.DataFrame = results[0]
79
+ lte_analysis_df: pd.DataFrame = results[1]
80
+ LteCapacity.final_results = convert_lte_analysis_dfs(
81
+ [lte_analysis_df, bh_report], ["LTE_Analysis", "LTE_BH_Report"]
82
+ )
83
+ st.download_button(
84
+ on_click="ignore",
85
+ type="primary",
86
+ label="Download the Analysis Report",
87
+ data=LteCapacity.final_results,
88
+ file_name="LTE_Analysis_Report.xlsx",
89
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
90
+ )
91
+ st.write(lte_analysis_df)
92
+ # Add dataframe and Pie chart with "final_comments" distribution
93
+ st.markdown("***")
94
+ st.markdown(":blue[**Final comment distribution**]")
95
+ final_comments_df = (
96
+ lte_analysis_df.groupby("final_comments")
97
+ .size()
98
+ .reset_index(name="count")
99
+ .sort_values(by="count", ascending=False)
100
+ )
101
+ final_comments_col1, final_comments_col2 = st.columns((1, 3))
102
+ with final_comments_col1:
103
+ st.write(final_comments_df)
104
+ with final_comments_col2:
105
+ fig = px.pie(
106
+ final_comments_df,
107
+ names="final_comments",
108
+ values="count",
109
+ hover_name="final_comments",
110
+ hover_data=["count"],
111
+ title="Final comment distribution",
112
+ )
113
+ fig.update_layout(height=600)
114
+ fig.update_traces(
115
+ texttemplate="%{label}: %{value}",
116
+ textfont_size=15,
117
+ textposition="outside",
118
+ )
119
+ st.plotly_chart(fig, use_container_width=True)
120
+ # Add dataframe and Pie chart with "final_comments" distribution where num_congested_cells > 0
121
+ st.markdown("***")
122
+ st.markdown(":blue[**Congested cells distribution**]")
123
+ congested_cells_df = (
124
+ lte_analysis_df[lte_analysis_df["num_congested_cells"] > 0]
125
+ .groupby("final_comments")
126
+ .size()
127
+ .reset_index(name="count")
128
+ .sort_values(by="count", ascending=False)
129
+ )
130
+ congested_cells_col1, congested_cells_col2 = st.columns((1, 3))
131
+ with congested_cells_col1:
132
+ st.write(congested_cells_df)
133
+ with congested_cells_col2:
134
+ fig = px.pie(
135
+ congested_cells_df,
136
+ names="final_comments",
137
+ values="count",
138
+ hover_name="final_comments",
139
+ hover_data=["count"],
140
+ title="Congested cells distribution",
141
+ )
142
+ fig.update_layout(height=600)
143
+ fig.update_traces(
144
+ texttemplate="%{label}: %{value}",
145
+ textfont_size=15,
146
+ textposition="outside",
147
+ )
148
+ st.plotly_chart(fig, use_container_width=True)
149
+
150
+ # Add dataframe and Bar chart with "final_comments" distribution where num_congested_cells > 0 per Region
151
+ st.markdown("***")
152
+ st.markdown(":blue[**Congested cells distribution per Region**]")
153
+ congested_cells_region_df = (
154
+ lte_analysis_df[lte_analysis_df["num_congested_cells"] > 0]
155
+ .groupby(["Region", "final_comments"])
156
+ .size()
157
+ .reset_index(name="count")
158
+ .sort_values(by="count", ascending=False)
159
+ )
160
+ congested_cells_region_col1, congested_cells_region_col2 = st.columns(
161
+ (1, 3)
162
+ )
163
+ with congested_cells_region_col1:
164
+ st.write(congested_cells_region_df)
165
+ with congested_cells_region_col2:
166
+ fig = px.bar(
167
+ congested_cells_region_df,
168
+ x="Region",
169
+ y="count",
170
+ color="final_comments",
171
+ title="Congested cells distribution per Region",
172
+ )
173
+ fig.update_layout(height=600)
174
+ fig.update_traces(
175
+ texttemplate="%{value}", textfont_size=15, textposition="outside"
176
+ )
177
+ st.plotly_chart(fig, use_container_width=True)
178
+ # Add dataframe and Bar chart with "final_comments" distribution where num_congested_cells > 0 per Region groupby region
179
+ st.markdown("***")
180
+ st.markdown(
181
+ ":blue[**Congested cells distribution per Region groupby Region**]"
182
+ )
183
+ congested_cells_region_groupby_region_df = (
184
+ lte_analysis_df[lte_analysis_df["num_congested_cells"] > 0]
185
+ .groupby(["Region"])
186
+ .size()
187
+ .reset_index(name="count")
188
+ .sort_values(by="count", ascending=False)
189
+ )
190
+ (
191
+ congested_cells_region_groupby_region_col1,
192
+ congested_cells_region_groupby_region_col2,
193
+ ) = st.columns((1, 3))
194
+ with congested_cells_region_groupby_region_col1:
195
+ st.write(congested_cells_region_groupby_region_df)
196
+ with congested_cells_region_groupby_region_col2:
197
+ fig = px.bar(
198
+ congested_cells_region_groupby_region_df,
199
+ x="Region",
200
+ y="count",
201
+ title="Congested cells distribution per Region groupby Region",
202
+ )
203
+ fig.update_layout(height=600)
204
+ fig.update_traces(
205
+ texttemplate="%{value}", textfont_size=15, textposition="outside"
206
+ )
207
+ st.plotly_chart(fig, use_container_width=True)
assets/lte_capacity.png ADDED
documentations/lte_capacity_docs.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ st.markdown(
4
+ """
5
+ # LTE Capacity Analysis Documentation
6
+
7
+ This documentation provides a technical and practical reference for the LTE Capacity Analysis application, detailing input/output columns, processing workflow, and key metrics as implemented in:
8
+ - apps/kpi_analysis/lte_capacity.py
9
+ - process_kpi/process_lte_capacity.py
10
+ - utils/kpi_analysis_utils.py
11
+
12
+ ---
13
+
14
+ ## 1. Input Files and Expected Columns
15
+
16
+ ### a. Dump File (XLSB)
17
+ - Contains network configuration and site data.
18
+ - Expected columns (see `LTE_DATABASE_COLUMNS` in `process_lte_capacity.py`):
19
+ - code: Unique site identifier
20
+ - Region: Geographical region of the site
21
+ - site_config_band: Configured frequency bands at the site
22
+ - final_name: Formatted site name
23
+
24
+ ### b. Busy Hour (BH) KPI Report (CSV)
25
+ - Contains performance metrics for LTE cells during busy hours.
26
+ - Key columns (see `KPI_COLUMNS` in `process_lte_capacity.py`):
27
+ - date: Timestamp of the measurement
28
+ - LNCEL_name: Cell identifier (format: SiteName_LBand_CellID)
29
+ - Cell_Avail_excl_BLU: Cell availability percentage excluding BLU
30
+ - E_UTRAN_Avg_PRB_usage_per_TTI_DL: Average Physical Resource Block usage in downlink
31
+
32
+ ---
33
+
34
+ ## 2. Output Columns and Their Meaning
35
+
36
+ ### a. LTE Analysis Output (`LTE_ANALYSIS_COLUMNS`):
37
+ - **Site Information**:
38
+ - code: Site identifier
39
+ - Region: Geographical region
40
+ - site_config_band: Configured frequency bands
41
+
42
+ - **Cell Configuration**:
43
+ - LNCEL_name_l800: Cell name for 800MHz band
44
+ - LNCEL_name_l1800: Cell name for 1800MHz band
45
+ - LNCEL_name_l2300: Cell name for 2300MHz band
46
+ - LNCEL_name_l2600: Cell name for 2600MHz band
47
+ - LNCEL_name_l1800s: Cell name for 1800MHz supplementary band
48
+
49
+ - **PRB Usage Metrics**:
50
+ - avg_prb_usage_bh_l800: Average PRB usage for 800MHz band
51
+ - avg_prb_usage_bh_l1800: Average PRB usage for 1800MHz band
52
+ - avg_prb_usage_bh_l2300: Average PRB usage for 2300MHz band
53
+ - avg_prb_usage_bh_l2600: Average PRB usage for 2600MHz band
54
+ - avg_prb_usage_bh_l1800s: Average PRB usage for 1800s band
55
+
56
+ - **Cell Status**:
57
+ - num_congested_cells: Number of cells exceeding PRB usage threshold
58
+ - num_cells: Total number of cells at the site
59
+ - num_cell_with_kpi: Number of cells with valid KPI data
60
+ - num_down_or_no_kpi_cells: Number of down or non-reporting cells
61
+ - prb_diff_between_cells: Maximum PRB usage difference between cells at the site
62
+ - load_balance_required: Flag indicating if load balancing is needed
63
+
64
+ - **Analysis Results**:
65
+ - congestion_comment: Comments on cell congestion status
66
+ - final_comments: Summary of site status and recommendations
67
+
68
+ ---
69
+
70
+ ## 3. Processing Workflow
71
+
72
+ 1. **Data Loading and Validation**:
73
+ - Load and validate the dump file and BH report
74
+ - Check for required columns and data integrity
75
+
76
+ 2. **Data Processing**:
77
+ - Parse site and cell information from the dump file
78
+ - Process KPI data from the BH report
79
+ - Calculate average PRB usage per cell and band
80
+
81
+ 3. **Analysis**:
82
+ - Identify congested cells based on PRB usage threshold
83
+ - Calculate load balancing requirements
84
+ - Determine site-level congestion status
85
+ - Generate recommendations for capacity expansion
86
+
87
+ 4. **Reporting**:
88
+ - Combine all analysis results into a comprehensive DataFrame
89
+ - Generate final comments and recommendations
90
+ - Prepare data for visualization and export
91
+
92
+ ---
93
+
94
+ ## 4. Key Functions
95
+
96
+ ### a. `process_lte_bh_report` (in `process_lte_capacity.py`)
97
+ - Main function that orchestrates the LTE capacity analysis
98
+ - Parameters:
99
+ - dump_path: Path to the site dump file
100
+ - bh_report_path: Path to the Busy Hour KPI report
101
+ - num_last_days: Number of days to analyze
102
+ - num_threshold_days: Number of days for threshold calculations
103
+ - availability_threshold: Minimum required cell availability (%)
104
+ - prb_usage_threshold: Threshold for PRB usage (%)
105
+ - prb_diff_between_cells_threshold: Maximum allowed PRB difference between cells (%)
106
+
107
+ ### b. `lte_analysis_logic` (in `process_lte_capacity.py`)
108
+ - Core logic for analyzing LTE capacity
109
+ - Identifies congested cells and calculates load balancing requirements
110
+ - Generates comments and recommendations
111
+ ### c. analyze_prb_usage (in kpi_analysis_utils.py)
112
+ - Analyzes PRB usage patterns
113
+ - Identifies cells with high PRB utilization
114
+ - Generates comments on congestion status
115
+
116
+ ### d. cell_availability_analysis (in kpi_analysis_utils.py)
117
+ - Analyzes cell availability metrics
118
+ - Identifies cells with availability issues
119
+ - Generates availability-related comments
120
+
121
+ ---
122
+
123
+ ## 5. Configuration Parameters
124
+
125
+ ### a. Band Mapping (from LteCapacity class):
126
+ - Defines the recommended next band for capacity expansion
127
+ - Example: L1800 โ†’ L800, L800 โ†’ L1800, etc.
128
+
129
+ ### b. Thresholds (configurable via UI/parameters):
130
+ - Availability Threshold: Default 95%
131
+ - PRB Usage Threshold: Default 80%
132
+ - PRB Difference Threshold: Default 20%
133
+ - Analysis Period: Default 7 days
134
+ - Threshold Days: Default 3 days
135
+
136
+ ---
137
+
138
+ ## 6. Example Usage and Output Analysis
139
+
140
+ ### Basic Usage
141
+ ```python
142
+ from process_kpi.process_lte_capacity import process_lte_bh_report
143
+ import pandas as pd
144
+
145
+ # Process LTE capacity analysis
146
+ results = process_lte_bh_report(
147
+ dump_path="network_dump_202305.xlsb",
148
+ bh_report_path="lte_bh_report_20230501_20230507.csv",
149
+ num_last_days=7, # Analyze last 7 days
150
+ num_threshold_days=3, # Consider threshold violations if seen on โ‰ฅ3 days
151
+ availability_threshold=95.0, # Minimum acceptable cell availability (%)
152
+ prb_usage_threshold=80.0, # PRB usage threshold for congestion (%)
153
+ prb_diff_between_cells_threshold=20.0 # Max allowed PRB difference between cells (%)
154
+ )
155
+
156
+ # Unpack results
157
+ bh_report_df, lte_analysis_df = results
158
+
159
+ # Example: Display sites with congestion
160
+ congested_sites = lte_analysis_df[lte_analysis_df['num_congested_cells'] > 0]
161
+ print(f"Found {len(congested_sites)} sites with congestion")
162
+
163
+ # Example: Export results to Excel
164
+ with pd.ExcelWriter('lte_capacity_analysis.xlsx') as writer:
165
+ lte_analysis_df.to_excel(writer, sheet_name='LTE_Analysis', index=False)
166
+ bh_report_df.to_excel(writer, sheet_name='BH_Report', index=False)
167
+ ```
168
+
169
+ ### Understanding the Output
170
+ - `lte_analysis_df`: Contains per-site analysis with capacity recommendations
171
+ - `bh_report_df`: Raw busy hour metrics for detailed investigation
172
+
173
+
174
+ ## 7. Column Reference Table
175
+
176
+ ### Site Information
177
+ | Column | Type | Description | Example |
178
+ |--------|------|-------------|---------|
179
+ | code | str | Unique site identifier | SITE123 |
180
+ | Region | str | Mali Geographical region | CENTRAL |
181
+ | site_config_band | str | Configured frequency bands | L1800/L800 |
182
+
183
+ ### Cell Configuration
184
+ | Column | Type | Description | Example |
185
+ |--------|------|-------------|---------|
186
+ | LNCEL_name_l800 | str | 800MHz cell name | SITE123_L800_1 |
187
+ | LNCEL_name_l1800 | str | 1800MHz cell name | SITE123_L1800_1 |
188
+ | LNCEL_name_l2300 | str | 2300MHz cell name | SITE123_L2300_1 |
189
+ | LNCEL_name_l2600 | str | 2600MHz cell name | SITE123_L2600_1 |
190
+ | LNCEL_name_l1800s | str | 1800s cell name | SITE123_L1800S_1 |
191
+
192
+ ### PRB Usage Metrics
193
+ | Column | Type | Description | Range |
194
+ |--------|------|-------------|-------|
195
+ | avg_prb_usage_bh_l800 | float | Avg PRB usage 800MHz | 0-100% |
196
+ | avg_prb_usage_bh_l1800 | float | Avg PRB usage 1800MHz | 0-100% |
197
+ | avg_prb_usage_bh_l2300 | float | Avg PRB usage 2300MHz | 0-100% |
198
+ | avg_prb_usage_bh_l2600 | float | Avg PRB usage 2600MHz | 0-100% |
199
+ | avg_prb_usage_bh_l1800s | float | Avg PRB usage 1800s | 0-100% |
200
+
201
+ ### Cell Status
202
+ | Column | Type | Description |
203
+ |--------|------|-------------|
204
+ | num_cells | int | Total cells at site |
205
+ | num_cell_with_kpi | int | Cells with valid KPI data |
206
+ | num_down_or_no_kpi_cells | int | Non-reporting cells |
207
+ | num_congested_cells | int | Cells exceeding PRB threshold |
208
+ | prb_diff_between_cells | float | Max PRB difference between cells |
209
+ | load_balance_required | bool | If load balancing is needed |
210
+
211
+ ### Analysis Results
212
+ | Column | Type | Description |
213
+ |--------|------|-------------|
214
+ | congestion_comment | str | Analysis of congestion status |
215
+ | final_comments | str | Summary and recommendations |
216
+ | recommended_action | str | Suggested capacity actions |
217
+ | next_band | str | Recommended band for expansion |
218
+
219
+
220
+ """
221
+ )
process_kpi/lte_kpi_requirements.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LTE CAPACITY REPORT
2
+
3
+ Based on gsm and wcdma exemple let's build LTE capacity report
4
+
5
+ ## Required Input
6
+
7
+ - File : LTE BH report with columns :
8
+ - PERIOD_START_TIME
9
+ - MRBTS/SBTS name
10
+ - LNBTS name
11
+ - LNCEL name
12
+ - DN
13
+ - Cell Avail excl BLU
14
+ - E-UTRAN Avg PRB usage per TTI DL
15
+ - Number of last day for the analysis
16
+ - Number of days for threshold
17
+ - Availability threshold
18
+ - PRB usage per TTI DL threshold
19
+ - Max difference between PRB usage over cells of the same BTS
20
+
21
+ ### TASK
22
+
23
+ - Pivot KPI in BH report per KPI (Cell Avail excl BLU, E-UTRAN Avg PRB usage per TTI DL)
24
+ - Calculate Average and Max of PRB usage per TTI DL
25
+ - Calculate Average and Max of Cell Avail excl BLU
26
+ - Count number of Days with Cell Avail excl BLU below Availability threshold
27
+ - Count number of Days with PRB usage per TTI DL exceeded PRB usage per TTI DL threshold
28
+ - Create separate DF per sector and band based on LNCEL name
29
+ - _1_L800: column_name = Sector_1_L800
30
+ - _2_L800: column_name = Sector_2_L800
31
+ - _3_L800: column_name = Sector_3_L800
32
+ - _1_L1800: column_name = Sector_1_L1800
33
+ - _2_L1800: column_name = Sector_2_L1800
34
+ - _3_L1800: column_name = Sector_3_L1800
35
+ - _1_L2300: column_name = Sector_1_L2300
36
+ - _2_L2300: column_name = Sector_2_L2300
37
+ - _3_L2300: column_name = Sector_3_L2300
38
+ - _1_L2600: column_name = Sector_1_L2600
39
+ - _2_L2600: column_name = Sector_2_L2600
40
+ - _3_L2600: column_name = Sector_3_L2600
41
+ - _1S_L1800: column_name = Sector_1S_L1800
42
+ - _2S_L1800: column_name = Sector_2S_L1800
43
+ - _3S_L1800: column_name = Sector_3S_L1800
44
+ - Merge DFs per sector LNBTS name
45
+ - Concat dfs per Bands
46
+
process_kpi/process_lte_capacity.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from queries.process_lte import process_lte_data
5
+ from utils.convert_to_excel import save_dataframe
6
+ from utils.kpi_analysis_utils import (
7
+ LteCapacity,
8
+ analyze_prb_usage,
9
+ cell_availability_analysis,
10
+ create_dfs_per_kpi,
11
+ create_hourly_date,
12
+ kpi_naming_cleaning,
13
+ )
14
+
15
+ LTE_ANALYSIS_COLUMNS = [
16
+ "code",
17
+ "code_sector",
18
+ "Region",
19
+ "site_config_band",
20
+ "LNCEL_name_l800",
21
+ "LNCEL_name_l1800",
22
+ "LNCEL_name_l2300",
23
+ "LNCEL_name_l2600",
24
+ "LNCEL_name_l1800s",
25
+ "avg_prb_usage_bh_l800",
26
+ "avg_prb_usage_bh_l1800",
27
+ "avg_prb_usage_bh_l2300",
28
+ "avg_prb_usage_bh_l2600",
29
+ "avg_prb_usage_bh_l1800s",
30
+ "num_congested_cells",
31
+ "num_cells",
32
+ "num_cell_with_kpi",
33
+ "num_down_or_no_kpi_cells",
34
+ "prb_diff_between_cells",
35
+ "load_balance_required",
36
+ "congestion_comment",
37
+ "final_comments",
38
+ ]
39
+
40
+ LTE_DATABASE_COLUMNS = [
41
+ "code",
42
+ "Region",
43
+ "site_config_band",
44
+ "final_name",
45
+ ]
46
+
47
+ KPI_COLUMNS = [
48
+ "date",
49
+ "LNCEL_name",
50
+ "Cell_Avail_excl_BLU",
51
+ "E_UTRAN_Avg_PRB_usage_per_TTI_DL",
52
+ "DL_PRB_Util_p_TTI_Lev_10",
53
+ ]
54
+ PRB_COLUMNS = [
55
+ "LNCEL_name",
56
+ "avg_prb_usage_bh",
57
+ # "avg_prb_usage_bh_lev_10",
58
+ ]
59
+
60
+
61
+ def lte_analysis_logic(
62
+ df: pd.DataFrame,
63
+ prb_usage_threshold: int,
64
+ prb_diff_between_cells_threshold: int,
65
+ ) -> pd.DataFrame:
66
+ lte_analysis_logic_df = df.copy()
67
+ lte_analysis_logic_df["num_congested_cells"] = (
68
+ lte_analysis_logic_df[
69
+ [
70
+ "avg_prb_usage_bh_l800",
71
+ "avg_prb_usage_bh_l1800",
72
+ "avg_prb_usage_bh_l2300",
73
+ "avg_prb_usage_bh_l2600",
74
+ "avg_prb_usage_bh_l1800s",
75
+ ]
76
+ ]
77
+ >= prb_usage_threshold
78
+ ).sum(axis=1)
79
+
80
+ # Add Number of cells LNCEL_name_l800 LNCEL_name_l1800 LNCEL_name_l2300 LNCEL_name_l2600 LNCEL_name_l1800s
81
+ lte_analysis_logic_df["num_cells"] = lte_analysis_logic_df[
82
+ [
83
+ "LNCEL_name_l800",
84
+ "LNCEL_name_l1800",
85
+ "LNCEL_name_l2300",
86
+ "LNCEL_name_l2600",
87
+ "LNCEL_name_l1800s",
88
+ ]
89
+ ].count(axis=1)
90
+
91
+ # Add Number of cell with KPI
92
+ lte_analysis_logic_df["num_cell_with_kpi"] = lte_analysis_logic_df[
93
+ [
94
+ "avg_prb_usage_bh_l800",
95
+ "avg_prb_usage_bh_l1800",
96
+ "avg_prb_usage_bh_l2300",
97
+ "avg_prb_usage_bh_l2600",
98
+ "avg_prb_usage_bh_l1800s",
99
+ ]
100
+ ].count(axis=1)
101
+
102
+ # Number of Down or No KPI cells = num_cells -num_cell_with_kpi
103
+ lte_analysis_logic_df["num_down_or_no_kpi_cells"] = (
104
+ lte_analysis_logic_df["num_cells"] - lte_analysis_logic_df["num_cell_with_kpi"]
105
+ )
106
+
107
+ # Check Max difference between avg_prb_usage_bh_l800 avg_prb_usage_bh_l1800 avg_prb_usage_bh_l2300 avg_prb_usage_bh_l2600 avg_prb_usage_bh_l1800s
108
+ lte_analysis_logic_df["prb_diff_between_cells"] = lte_analysis_logic_df[
109
+ [
110
+ "avg_prb_usage_bh_l800",
111
+ "avg_prb_usage_bh_l1800",
112
+ "avg_prb_usage_bh_l2300",
113
+ "avg_prb_usage_bh_l2600",
114
+ "avg_prb_usage_bh_l1800s",
115
+ ]
116
+ ].apply(lambda row: max(row) - min(row), axis=1)
117
+
118
+ # Add Load balance required column = Yes if prb_diff_between_cells > prb_diff_between_cells_threshold else No
119
+ lte_analysis_logic_df["load_balance_required"] = lte_analysis_logic_df[
120
+ "prb_diff_between_cells"
121
+ ].apply(lambda x: "Yes" if x > prb_diff_between_cells_threshold else "No")
122
+
123
+ # Add Next band column
124
+ lte_analysis_logic_df["next_band"] = lte_analysis_logic_df["site_config_band"].map(
125
+ LteCapacity.next_band_mapping
126
+ )
127
+
128
+ # Add congestion comments
129
+ # if num_congested_cells == 0 and num_down_or_no_kpi_cells == 0 = " No Congestion"
130
+ # if num_congested_cells == 0 and num_down_or_no_kpi_cells > 0 = "No congestion but Down cell"
131
+ # if num_congested_cells > 0 and num_down_or_no_kpi_cells > 0 = "Congestion but Colocated Down Cell"
132
+ # Else Need Action
133
+ conditions = [
134
+ (lte_analysis_logic_df["num_congested_cells"] == 0)
135
+ & (lte_analysis_logic_df["num_down_or_no_kpi_cells"] == 0),
136
+ (lte_analysis_logic_df["num_congested_cells"] == 0)
137
+ & (lte_analysis_logic_df["num_down_or_no_kpi_cells"] > 0),
138
+ (lte_analysis_logic_df["num_congested_cells"] > 0)
139
+ & (lte_analysis_logic_df["num_down_or_no_kpi_cells"] > 0),
140
+ ]
141
+
142
+ choices = [
143
+ "No Congestion",
144
+ "No congestion but Down cell",
145
+ "Congestion but Colocated Down Cell",
146
+ ]
147
+
148
+ lte_analysis_logic_df["congestion_comment"] = np.select(
149
+ conditions, choices, default="Need Action"
150
+ )
151
+
152
+ # Add "Actions" column
153
+ # if load_balance_required = "Yes" and congestion_comment = "Need Action" then "Load Balancing parameter tuning required"
154
+ # if load_balance_required = "Yes" and congestion_comment = "Need Action" then "Add Layer"
155
+ # Else keep congestion_comment
156
+ conditions = [
157
+ (lte_analysis_logic_df["load_balance_required"] == "Yes")
158
+ & (lte_analysis_logic_df["congestion_comment"] == "Need Action"),
159
+ (lte_analysis_logic_df["load_balance_required"] == "No")
160
+ & (lte_analysis_logic_df["congestion_comment"] == "Need Action"),
161
+ ]
162
+
163
+ choices = [
164
+ "Load Balancing parameter tuning required",
165
+ "Add Layer",
166
+ ]
167
+
168
+ lte_analysis_logic_df["actions"] = np.select(
169
+ conditions, choices, default=lte_analysis_logic_df["congestion_comment"]
170
+ )
171
+
172
+ # Add Final Comments
173
+ # if "actions" = "Add Layer" then "'Add' + 'next_band''
174
+ # Else keep "actions" as it is
175
+ lte_analysis_logic_df["final_comments"] = lte_analysis_logic_df.apply(
176
+ lambda row: (
177
+ f"Add {row['next_band']}"
178
+ if row["actions"] == "Add Layer"
179
+ else row["actions"]
180
+ ),
181
+ axis=1,
182
+ )
183
+
184
+ # create column "sector" equal to conteent of "LNCEL_name_l800" if not empty else "LNCEL_name_l1800" if not empty else "LNCEL_name_l2300"
185
+ lte_analysis_logic_df["sector"] = (
186
+ lte_analysis_logic_df["LNCEL_name_l800"]
187
+ .combine_first(lte_analysis_logic_df["LNCEL_name_l1800"])
188
+ .combine_first(lte_analysis_logic_df["LNCEL_name_l2300"])
189
+ .combine_first(lte_analysis_logic_df["LNCEL_name_l2600"])
190
+ .combine_first(lte_analysis_logic_df["LNCEL_name_l1800s"])
191
+ )
192
+ # remove rows where sector is empty
193
+ lte_analysis_logic_df = lte_analysis_logic_df[
194
+ lte_analysis_logic_df["sector"].notna()
195
+ ]
196
+ # Add sector_id column if sector contains : '_1_" then 1 elif sector contains : '_2_" then 2 elif sector contains : '_3_" then 3
197
+ lte_analysis_logic_df["sector_id"] = np.where(
198
+ lte_analysis_logic_df["sector"].str.contains("_1_"),
199
+ 1,
200
+ np.where(
201
+ lte_analysis_logic_df["sector"].str.contains("_2_"),
202
+ 2,
203
+ np.where(lte_analysis_logic_df["sector"].str.contains("_3_"), 3, np.nan),
204
+ ),
205
+ )
206
+ # add code_sector column by combine code and sector_id
207
+ lte_analysis_logic_df["code_sector"] = (
208
+ lte_analysis_logic_df["code"].astype(str)
209
+ + "_"
210
+ + lte_analysis_logic_df["sector_id"].astype(str)
211
+ )
212
+
213
+ # remove '.0' from code_sector
214
+ lte_analysis_logic_df["code_sector"] = lte_analysis_logic_df[
215
+ "code_sector"
216
+ ].str.replace(".0", "")
217
+
218
+ # lte_analysis_logic_df = lte_analysis_logic_df[LTE_ANALYSIS_COLUMNS]
219
+ return lte_analysis_logic_df
220
+
221
+
222
+ def dfs_per_band_cell(df: pd.DataFrame) -> pd.DataFrame:
223
+ # Base DataFrame with unique codes, Region, and site_config_band
224
+ all_codes_df = df[["code", "Region", "site_config_band"]].drop_duplicates()
225
+
226
+ # Configuration for sector groups and their respective LNCEL patterns and column suffixes
227
+ # Format: { "group_key": [(lncel_name_pattern_part, column_suffix), ...] }
228
+ # lncel_name_pattern_part will be combined with "_<group_key>" or similar
229
+ # Example: for group "1", pattern "_1_L800" gives suffix "l800"
230
+ sector_groups_config = {
231
+ "1": [
232
+ ("_1_L800", "l800"),
233
+ ("_1_L1800", "l1800"),
234
+ ("_1_L2300", "l2300"),
235
+ ("_1_L2600", "l2600"),
236
+ ("_1S_L1800", "l1800s"),
237
+ ],
238
+ "2": [
239
+ ("_2_L800", "l800"),
240
+ ("_2_L1800", "l1800"),
241
+ ("_2_L2300", "l2300"),
242
+ ("_2_L2600", "l2600"),
243
+ ("_2S_L1800", "l1800s"),
244
+ ],
245
+ "3": [
246
+ ("_3_L800", "l800"),
247
+ ("_3_L1800", "l1800"),
248
+ ("_3_L2300", "l2300"),
249
+ ("_3_L2600", "l2600"),
250
+ ("_3S_L1800", "l1800s"),
251
+ ],
252
+ }
253
+
254
+ all_processed_sectors_dfs = []
255
+
256
+ for sector_group_key, band_configurations in sector_groups_config.items():
257
+ # Start with the base DataFrame for the current sector group
258
+ current_sector_group_df = all_codes_df.copy()
259
+
260
+ for lncel_name_pattern, column_suffix in band_configurations:
261
+ # Filter the original DataFrame for the current LNCEL pattern
262
+ # The pattern assumes LNCEL_name contains something like "SITENAME<lncel_name_pattern>"
263
+ filtered_band_df = df[df["LNCEL_name"].str.contains(lncel_name_pattern)]
264
+
265
+ # Select relevant columns and rename them for the merge
266
+ # This avoids pandas automatically adding _x, _y suffixes and then needing to rename them
267
+ df_to_merge = filtered_band_df[
268
+ ["code", "LNCEL_name", "avg_prb_usage_bh"]
269
+ ].rename(
270
+ columns={
271
+ "LNCEL_name": f"LNCEL_name_{column_suffix}",
272
+ "avg_prb_usage_bh": f"avg_prb_usage_bh_{column_suffix}",
273
+ }
274
+ )
275
+
276
+ # Perform a left merge
277
+ current_sector_group_df = pd.merge(
278
+ current_sector_group_df, df_to_merge, on="code", how="left"
279
+ )
280
+
281
+ all_processed_sectors_dfs.append(current_sector_group_df)
282
+
283
+ # Concatenate all the processed sector DataFrames
284
+ all_sectors_dfs = pd.concat(all_processed_sectors_dfs, axis=0, ignore_index=True)
285
+
286
+ return all_sectors_dfs
287
+
288
+
289
+ def lte_database_for_capacity(dump_path: str):
290
+ dfs = process_lte_data(dump_path)
291
+ lte_fdd = dfs[0]
292
+ lte_tdd = dfs[1]
293
+
294
+ lte_fdd = lte_fdd[LTE_DATABASE_COLUMNS]
295
+ lte_tdd = lte_tdd[LTE_DATABASE_COLUMNS]
296
+
297
+ lte_db = pd.concat([lte_fdd, lte_tdd], axis=0)
298
+
299
+ # rename final_name to LNCEL_name
300
+ lte_db = lte_db.rename(columns={"final_name": "LNCEL_name"})
301
+
302
+ # save_dataframe(lte_db, "LTE_Database.csv")
303
+ return lte_db
304
+
305
+
306
+ def lte_bh_dfs_per_kpi(
307
+ dump_path: str,
308
+ df: pd.DataFrame,
309
+ number_of_kpi_days: int = 7,
310
+ availability_threshold: int = 95,
311
+ prb_usage_threshold: int = 80,
312
+ prb_diff_between_cells_threshold: int = 20,
313
+ number_of_threshold_days: int = 3,
314
+ ) -> pd.DataFrame:
315
+
316
+ # print(df.columns)
317
+
318
+ pivoted_kpi_dfs = create_dfs_per_kpi(
319
+ df=df,
320
+ pivot_date_column="date",
321
+ pivot_name_column="LNCEL_name",
322
+ kpi_columns_from=2,
323
+ )
324
+ cell_availability_df = cell_availability_analysis(
325
+ df=pivoted_kpi_dfs["Cell_Avail_excl_BLU"],
326
+ days=number_of_kpi_days,
327
+ availability_threshold=availability_threshold,
328
+ )
329
+ # prb_usage_df = analyze_prb_usage(
330
+ # df=pivoted_kpi_dfs["E_UTRAN_Avg_PRB_usage_per_TTI_DL"],
331
+ # number_of_kpi_days=number_of_kpi_days,
332
+ # prb_usage_threshold=prb_usage_threshold,
333
+ # analysis_type="BH",
334
+ # number_of_threshold_days=number_of_threshold_days,
335
+ # )
336
+ prb_lev10_usage_df = analyze_prb_usage(
337
+ df=pivoted_kpi_dfs["DL_PRB_Util_p_TTI_Lev_10"],
338
+ number_of_kpi_days=number_of_kpi_days,
339
+ prb_usage_threshold=prb_usage_threshold,
340
+ analysis_type="BH",
341
+ number_of_threshold_days=number_of_threshold_days,
342
+ )
343
+
344
+ bh_kpi_df = pd.concat([cell_availability_df, prb_lev10_usage_df], axis=1)
345
+ bh_kpi_df = bh_kpi_df.reset_index()
346
+ prb_df = bh_kpi_df[PRB_COLUMNS]
347
+
348
+ # drop row if lnCEL_name is empty or 1
349
+ prb_df = prb_df[prb_df["LNCEL_name"].str.len() > 3]
350
+ # prb_df = prb_df.reset_index()
351
+ prb_df = prb_df.droplevel(level=1, axis=1) # Drop the first level (date)
352
+ # prb_df = prb_df.reset_index()
353
+ # prb_df["code"] = prb_df["LNCEL_name"].str.split("_").str[0]
354
+
355
+ lte_db = lte_database_for_capacity(dump_path)
356
+
357
+ db_and_prb = pd.merge(lte_db, prb_df, on="LNCEL_name", how="left")
358
+
359
+ # if avg_prb_usage_bh is "" then set it to "cell exists in dump but not in BH report"
360
+ # db_and_prb.loc[db_and_prb["avg_prb_usage_bh"].isnull(), "avg_prb_usage_bh"] = (
361
+ # "cell exists in dump but not in BH report"
362
+ # )
363
+ # drop row if lnCEL_name is empty or 1
364
+ db_and_prb = db_and_prb[db_and_prb["LNCEL_name"].str.len() > 3]
365
+
366
+ lte_analysis_df = dfs_per_band_cell(db_and_prb)
367
+ lte_analysis_df = lte_analysis_logic(
368
+ lte_analysis_df,
369
+ prb_usage_threshold,
370
+ prb_diff_between_cells_threshold,
371
+ )
372
+ lte_analysis_df = lte_analysis_df[LTE_ANALYSIS_COLUMNS]
373
+
374
+ return [bh_kpi_df, lte_analysis_df]
375
+
376
+
377
+ def process_lte_bh_report(
378
+ dump_path: str,
379
+ bh_report_path: str,
380
+ num_last_days: int,
381
+ num_threshold_days: int,
382
+ availability_threshold: float,
383
+ prb_usage_threshold: float,
384
+ prb_diff_between_cells_threshold: float,
385
+ ) -> dict:
386
+ """
387
+ Process LTE Busy Hour report and perform capacity analysis
388
+
389
+ Args:
390
+ bh_report_path: Path to BH report CSV file
391
+ num_last_days: Number of last days for analysis
392
+ num_threshold_days: Number of days for threshold calculation
393
+ availability_threshold: Minimum required availability
394
+ prb_usage_threshold: Maximum allowed PRB usage
395
+ prb_diff_between_cells_threshold: Maximum allowed PRB usage difference between cells
396
+
397
+ Returns:
398
+ Dictionary containing analysis results and DataFrames
399
+ """
400
+ LteCapacity.final_results = None
401
+ # lte_db_dfs = lte_database_for_capacity(dump_path)
402
+
403
+ # Read BH report
404
+ df = pd.read_csv(bh_report_path, delimiter=";")
405
+ df = kpi_naming_cleaning(df)
406
+ # print(df.columns)
407
+ df = create_hourly_date(df)
408
+ df = df[KPI_COLUMNS]
409
+ pivoted_kpi_dfs = lte_bh_dfs_per_kpi(
410
+ dump_path=dump_path,
411
+ df=df,
412
+ number_of_kpi_days=num_last_days,
413
+ availability_threshold=availability_threshold,
414
+ prb_usage_threshold=prb_usage_threshold,
415
+ prb_diff_between_cells_threshold=prb_diff_between_cells_threshold,
416
+ number_of_threshold_days=num_threshold_days,
417
+ )
418
+
419
+ # save_dataframe(pivoted_kpi_dfs, "LTE_BH_Report.csv")
420
+ return pivoted_kpi_dfs
utils/convert_to_excel.py CHANGED
@@ -143,14 +143,31 @@ def get_format_map_by_format_type(formats: dict, format_type: str) -> dict:
143
  "number_trx_per_bcf": formats["blue_light"],
144
  "number_trx_per_site": formats["blue_light"],
145
  }
146
- # elif format_type == "LTE":
147
- # return {
148
- # "DL PRB Utilization": formats["orange"],
149
- # "UL PRB Utilization": formats["orange"],
150
- # "RSRP": formats["blue_light"],
151
- # "RSRQ": formats["blue_light"],
152
- # "Throughput (Mbps)": formats["green"],
153
- # }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  else:
155
  return {} # No formatting if format_type not matched
156
 
@@ -193,6 +210,11 @@ def convert_gsm_dfs(dfs, sheet_names) -> bytes:
193
  return _write_to_excel(dfs, sheet_names, index=True, format_type="GSM_Analysis")
194
 
195
 
 
 
 
 
 
196
  @st.cache_data
197
  def convert_database_dfs(dfs, sheet_names) -> bytes:
198
  return _write_to_excel(dfs, sheet_names, index=True, format_type="database")
 
143
  "number_trx_per_bcf": formats["blue_light"],
144
  "number_trx_per_site": formats["blue_light"],
145
  }
146
+ elif format_type == "LTE_Analysis":
147
+ return {
148
+ "code": formats["blue"],
149
+ "code_sector": formats["blue"],
150
+ "Region": formats["blue"],
151
+ "site_config_band": formats["blue"],
152
+ "LNCEL_name_l800": formats["beurre"],
153
+ "LNCEL_name_l1800": formats["purple5"],
154
+ "LNCEL_name_l2300": formats["purple6"],
155
+ "LNCEL_name_l2600": formats["blue_light"],
156
+ "LNCEL_name_l1800s": formats["gray"],
157
+ "avg_prb_usage_bh_l800": formats["beurre"],
158
+ "avg_prb_usage_bh_l1800": formats["purple5"],
159
+ "avg_prb_usage_bh_l2300": formats["purple6"],
160
+ "avg_prb_usage_bh_l2600": formats["blue_light"],
161
+ "avg_prb_usage_bh_l1800s": formats["gray"],
162
+ "num_congested_cells": formats["orange"],
163
+ "num_cells": formats["orange"],
164
+ "num_cell_with_kpi": formats["orange"],
165
+ "num_down_or_no_kpi_cells": formats["orange"],
166
+ "prb_diff_between_cells": formats["orange"],
167
+ "load_balance_required": formats["orange"],
168
+ "congestion_comment": formats["orange"],
169
+ "final_comments": formats["green"],
170
+ }
171
  else:
172
  return {} # No formatting if format_type not matched
173
 
 
210
  return _write_to_excel(dfs, sheet_names, index=True, format_type="GSM_Analysis")
211
 
212
 
213
+ @st.cache_data
214
+ def convert_lte_analysis_dfs(dfs, sheet_names) -> bytes:
215
+ return _write_to_excel(dfs, sheet_names, index=True, format_type="LTE_Analysis")
216
+
217
+
218
  @st.cache_data
219
  def convert_database_dfs(dfs, sheet_names) -> bytes:
220
  return _write_to_excel(dfs, sheet_names, index=True, format_type="database")
utils/kpi_analysis_utils.py CHANGED
@@ -538,3 +538,50 @@ def analyze_sdcch_call_blocking(
538
  )
539
 
540
  return result_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  )
539
 
540
  return result_df
541
+
542
+
543
+ class LteCapacity:
544
+ final_results = None
545
+ # Next band mapping
546
+ next_band_mapping = {
547
+ "L1800": "L800",
548
+ "L800": "L1800",
549
+ "L1800/L800": "L2600",
550
+ "L1800/L2300/L800": "L2600",
551
+ "L2300/L800": "L2600",
552
+ "L1800/L2600/L800": "New site/Dual Beam",
553
+ "L1800/L2300/L2600/L800": "New site/Dual Beam",
554
+ "L2300": "FDD H// colocated site",
555
+ }
556
+
557
+
558
+ def analyze_prb_usage(
559
+ df: pd.DataFrame,
560
+ number_of_kpi_days: int,
561
+ prb_usage_threshold: int,
562
+ analysis_type: str,
563
+ number_of_threshold_days: int,
564
+ ) -> pd.DataFrame:
565
+ result_df = df.copy()
566
+ last_days_df: pd.DataFrame = result_df.iloc[:, -number_of_kpi_days:]
567
+ # last_days_df = last_days_df.fillna(0)
568
+
569
+ result_df[f"avg_prb_usage_{analysis_type.lower()}"] = last_days_df.mean(
570
+ axis=1
571
+ ).round(2)
572
+ result_df[f"max_prb_usage_{analysis_type.lower()}"] = last_days_df.max(axis=1)
573
+ # Count the number of days above threshold
574
+ result_df[f"number_of_days_with_prb_usage_exceeded_{analysis_type.lower()}"] = (
575
+ last_days_df.apply(
576
+ lambda row: sum(1 for x in row if x >= prb_usage_threshold), axis=1
577
+ )
578
+ )
579
+
580
+ # Add the daily_prb_comment : if number_of_days_with_prb_usage_exceeded_daily is >= number_of_threshold_days : prb usage exceeded threshold , else : None
581
+ result_df[f"prb_usage_{analysis_type.lower()}_comment"] = np.where(
582
+ result_df[f"number_of_days_with_prb_usage_exceeded_{analysis_type.lower()}"]
583
+ >= number_of_threshold_days,
584
+ "PRB usage exceeded threshold",
585
+ None,
586
+ )
587
+ return result_df