priorityx 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- priorityx/__init__.py +7 -0
- priorityx/core/__init__.py +0 -0
- priorityx/core/filters.py +224 -0
- priorityx/core/glmm.py +377 -0
- priorityx/core/quadrants.py +60 -0
- priorityx/drivers/__init__.py +0 -0
- priorityx/tracking/__init__.py +0 -0
- priorityx/tracking/drivers.py +569 -0
- priorityx/tracking/movement.py +483 -0
- priorityx/tracking/transitions.py +214 -0
- priorityx/utils/__init__.py +0 -0
- priorityx/utils/helpers.py +226 -0
- priorityx/viz/__init__.py +0 -0
- priorityx/viz/matrix.py +314 -0
- priorityx/viz/timeline.py +302 -0
- priorityx/viz/trajectory.py +231 -0
- priorityx-0.1.2.dist-info/METADATA +73 -0
- priorityx-0.1.2.dist-info/RECORD +20 -0
- priorityx-0.1.2.dist-info/WHEEL +4 -0
- priorityx-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Transition timeline heatmap visualization."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Literal, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plot_transition_timeline(
|
|
10
|
+
transitions_df: pd.DataFrame,
|
|
11
|
+
entity_filter: Optional[List[str]] = None,
|
|
12
|
+
highlight_risk_levels: List[str] = ["critical", "high"],
|
|
13
|
+
filter_risk_levels: List[str] = ["critical", "high", "medium"],
|
|
14
|
+
max_entities: Optional[int] = 20,
|
|
15
|
+
figsize: Tuple[int, int] = (14, 8),
|
|
16
|
+
title: Optional[str] = None,
|
|
17
|
+
x_axis_granularity: Literal["quarterly", "semiannual", "yearly"] = "quarterly",
|
|
18
|
+
sort_by_risk_first: bool = True,
|
|
19
|
+
entity_name: str = "Entity",
|
|
20
|
+
show_all_periods: bool = False,
|
|
21
|
+
save_plot: bool = False,
|
|
22
|
+
save_csv: bool = False,
|
|
23
|
+
output_dir: str = "plot",
|
|
24
|
+
temporal_granularity: str = "quarterly",
|
|
25
|
+
) -> plt.Figure:
|
|
26
|
+
"""
|
|
27
|
+
Visualize transition timeline as heatmap.
|
|
28
|
+
|
|
29
|
+
Creates a timeline showing when entities transition between quadrants,
|
|
30
|
+
color-coded by risk level.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
transitions_df: DataFrame from extract_transitions()
|
|
34
|
+
Required columns: entity, transition_quarter, from_quadrant,
|
|
35
|
+
to_quadrant, risk_level
|
|
36
|
+
entity_filter: Optional list of entities to include
|
|
37
|
+
highlight_risk_levels: Risk levels to highlight (default: critical, high)
|
|
38
|
+
filter_risk_levels: Only show these risk levels (default: critical, high, medium)
|
|
39
|
+
max_entities: Maximum entities to display (default: 20)
|
|
40
|
+
figsize: Figure size (width, height)
|
|
41
|
+
title: Optional custom title
|
|
42
|
+
x_axis_granularity: Time grouping ('quarterly', 'semiannual', 'yearly')
|
|
43
|
+
sort_by_risk_first: Sort by risk level first (True) or count first (False)
|
|
44
|
+
entity_name: Name for y-axis label
|
|
45
|
+
show_all_periods: Include starting period even if no transitions
|
|
46
|
+
save_plot: Save plot to file
|
|
47
|
+
save_csv: Save data to CSV
|
|
48
|
+
output_dir: Output directory for saved files
|
|
49
|
+
temporal_granularity: Time granularity for file naming
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Matplotlib figure
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
>>> # basic timeline
|
|
56
|
+
>>> fig = plot_transition_timeline(transitions_df, entity_name="Service")
|
|
57
|
+
|
|
58
|
+
>>> # focus on critical transitions only
|
|
59
|
+
>>> fig = plot_transition_timeline(
|
|
60
|
+
... transitions_df,
|
|
61
|
+
... filter_risk_levels=["critical"],
|
|
62
|
+
... max_entities=10
|
|
63
|
+
... )
|
|
64
|
+
"""
|
|
65
|
+
if transitions_df.empty:
|
|
66
|
+
print("No transitions to visualize")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
# filter out invalid entities
|
|
70
|
+
df = transitions_df[
|
|
71
|
+
transitions_df["entity"].notna() & (transitions_df["entity"] != "nan")
|
|
72
|
+
].copy()
|
|
73
|
+
|
|
74
|
+
if df.empty:
|
|
75
|
+
print("No valid transitions to visualize")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# filter by risk levels
|
|
79
|
+
if filter_risk_levels:
|
|
80
|
+
df = df[df["risk_level"].isin(filter_risk_levels)]
|
|
81
|
+
if df.empty:
|
|
82
|
+
print(f"No transitions with risk levels: {filter_risk_levels}")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# apply entity filter
|
|
86
|
+
if entity_filter:
|
|
87
|
+
df = df[df["entity"].isin(entity_filter)]
|
|
88
|
+
|
|
89
|
+
# map risk levels to numeric order
|
|
90
|
+
risk_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
91
|
+
df["risk_order"] = df["risk_level"].map(risk_order)
|
|
92
|
+
|
|
93
|
+
# aggregate quarters based on granularity
|
|
94
|
+
if x_axis_granularity == "semiannual":
|
|
95
|
+
def quarter_to_semester(q_str):
|
|
96
|
+
if "-Q" in q_str:
|
|
97
|
+
year, quarter = q_str.split("-Q")
|
|
98
|
+
semester = "H1" if int(quarter) <= 2 else "H2"
|
|
99
|
+
return f"{year}-{semester}"
|
|
100
|
+
return q_str
|
|
101
|
+
df["period"] = df["transition_quarter"].apply(quarter_to_semester)
|
|
102
|
+
elif x_axis_granularity == "yearly":
|
|
103
|
+
def quarter_to_year(q_str):
|
|
104
|
+
if "-Q" in q_str:
|
|
105
|
+
return q_str.split("-Q")[0]
|
|
106
|
+
return q_str
|
|
107
|
+
df["period"] = df["transition_quarter"].apply(quarter_to_year)
|
|
108
|
+
else: # quarterly
|
|
109
|
+
df["period"] = df["transition_quarter"]
|
|
110
|
+
|
|
111
|
+
periods = sorted(df["period"].unique())
|
|
112
|
+
|
|
113
|
+
# add starting period if requested
|
|
114
|
+
if show_all_periods and len(periods) > 0:
|
|
115
|
+
first_period = periods[0]
|
|
116
|
+
if x_axis_granularity == "semiannual" and "-H" in first_period:
|
|
117
|
+
year, semester = first_period.split("-H")
|
|
118
|
+
prev_semester = "H1" if semester == "H2" else "H2"
|
|
119
|
+
prev_year = str(int(year) - 1) if prev_semester == "H2" else year
|
|
120
|
+
prev_period = f"{prev_year}-{prev_semester}"
|
|
121
|
+
if prev_period not in periods:
|
|
122
|
+
periods = [prev_period] + periods
|
|
123
|
+
elif x_axis_granularity == "quarterly" and "-Q" in first_period:
|
|
124
|
+
year, quarter = first_period.split("-Q")
|
|
125
|
+
prev_q = str((int(quarter) - 2) % 4 + 1)
|
|
126
|
+
prev_year = year if int(quarter) > 1 else str(int(year) - 1)
|
|
127
|
+
prev_period = f"{prev_year}-Q{prev_q}"
|
|
128
|
+
if prev_period not in periods:
|
|
129
|
+
periods = [prev_period] + periods
|
|
130
|
+
|
|
131
|
+
# sort entities by priority
|
|
132
|
+
if sort_by_risk_first:
|
|
133
|
+
# sort by: most recent activity, then risk level
|
|
134
|
+
entity_max_period = df.groupby("entity")["period"].max()
|
|
135
|
+
period_rank = {p: i for i, p in enumerate(periods)}
|
|
136
|
+
entity_priority = pd.DataFrame({
|
|
137
|
+
"latest_period": entity_max_period,
|
|
138
|
+
"period_rank": entity_max_period.map(period_rank),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
# get risk level in most recent period
|
|
142
|
+
def get_latest_risk(entity):
|
|
143
|
+
latest_p = entity_priority.loc[entity, "latest_period"]
|
|
144
|
+
entity_trans = df[(df["entity"] == entity) & (df["period"] == latest_p)]
|
|
145
|
+
return entity_trans["risk_order"].min() if len(entity_trans) > 0 else 99
|
|
146
|
+
|
|
147
|
+
entity_priority["risk_order"] = entity_priority.index.map(get_latest_risk)
|
|
148
|
+
entity_priority = entity_priority.sort_values(
|
|
149
|
+
["period_rank", "risk_order"],
|
|
150
|
+
ascending=[False, True]
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
# sort by count/volume
|
|
154
|
+
entity_priority = df.groupby("entity").agg({"risk_order": "min"})
|
|
155
|
+
entity_priority = entity_priority.sort_values("risk_order")
|
|
156
|
+
|
|
157
|
+
# limit to top N entities
|
|
158
|
+
if max_entities and len(entity_priority) > max_entities:
|
|
159
|
+
top_entities = entity_priority.head(max_entities).index.tolist()
|
|
160
|
+
df = df[df["entity"].isin(top_entities)]
|
|
161
|
+
entity_priority = entity_priority.loc[top_entities]
|
|
162
|
+
|
|
163
|
+
# get entities in priority order
|
|
164
|
+
entities = [e for e in entity_priority.index if e in df["entity"].values]
|
|
165
|
+
|
|
166
|
+
# create y-axis positions
|
|
167
|
+
y_positions = {entity: i for i, entity in enumerate(entities)}
|
|
168
|
+
|
|
169
|
+
# color map for risk levels
|
|
170
|
+
risk_colors = {
|
|
171
|
+
"critical": "#d62728", # red
|
|
172
|
+
"high": "#ff7f0e", # orange
|
|
173
|
+
"medium": "#ffdd57", # yellow
|
|
174
|
+
"low": "#2ca02c", # green
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# create figure
|
|
178
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
179
|
+
|
|
180
|
+
# plot transitions
|
|
181
|
+
for _, transition in df.iterrows():
|
|
182
|
+
y_pos = y_positions[transition["entity"]]
|
|
183
|
+
x_pos = periods.index(transition["period"])
|
|
184
|
+
|
|
185
|
+
color = risk_colors.get(transition["risk_level"], "#95a5a6")
|
|
186
|
+
|
|
187
|
+
# plot circle
|
|
188
|
+
ax.scatter(x_pos, y_pos, s=100, c=color, alpha=0.8, zorder=2)
|
|
189
|
+
|
|
190
|
+
# add transition label
|
|
191
|
+
label = f"{transition['from_quadrant']}->{transition['to_quadrant']}"
|
|
192
|
+
ax.annotate(
|
|
193
|
+
label,
|
|
194
|
+
(x_pos, y_pos),
|
|
195
|
+
xytext=(8, 0),
|
|
196
|
+
textcoords="offset points",
|
|
197
|
+
fontsize=9,
|
|
198
|
+
color="black",
|
|
199
|
+
va="center",
|
|
200
|
+
alpha=0.8,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# customize plot
|
|
204
|
+
ax.set_yticks(range(len(entities)))
|
|
205
|
+
ax.set_yticklabels(entities)
|
|
206
|
+
ax.invert_yaxis() # highest priority at top
|
|
207
|
+
|
|
208
|
+
# x-axis labels
|
|
209
|
+
if x_axis_granularity == "quarterly":
|
|
210
|
+
quarter_to_month = {"Q1": "Mar", "Q2": "Jun", "Q3": "Sep", "Q4": "Dec"}
|
|
211
|
+
period_labels = []
|
|
212
|
+
for p in periods:
|
|
213
|
+
if "-Q" in p:
|
|
214
|
+
year, quarter = p.split("-Q")
|
|
215
|
+
month = quarter_to_month.get(f"Q{quarter}", quarter)
|
|
216
|
+
period_labels.append(f"{month} {year}")
|
|
217
|
+
else:
|
|
218
|
+
period_labels.append(p)
|
|
219
|
+
elif x_axis_granularity == "semiannual":
|
|
220
|
+
period_labels = []
|
|
221
|
+
for p in periods:
|
|
222
|
+
if "-H" in p:
|
|
223
|
+
year, semester = p.split("-H")
|
|
224
|
+
period_labels.append(f"H{semester} {year}")
|
|
225
|
+
else:
|
|
226
|
+
period_labels.append(p)
|
|
227
|
+
else: # yearly
|
|
228
|
+
period_labels = periods
|
|
229
|
+
|
|
230
|
+
ax.set_xticks(range(len(periods)))
|
|
231
|
+
ax.set_xticklabels(period_labels, rotation=45, ha="right")
|
|
232
|
+
ax.set_xlim(-0.5, len(periods) - 0.5)
|
|
233
|
+
|
|
234
|
+
# clean up borders
|
|
235
|
+
ax.spines["left"].set_visible(False)
|
|
236
|
+
ax.spines["right"].set_visible(False)
|
|
237
|
+
ax.spines["top"].set_visible(False)
|
|
238
|
+
|
|
239
|
+
# build legend
|
|
240
|
+
legend_elements = []
|
|
241
|
+
for risk_level in ["critical", "high", "medium", "low"]:
|
|
242
|
+
if risk_level in df["risk_level"].values:
|
|
243
|
+
legend_elements.append(
|
|
244
|
+
plt.scatter([], [], s=100, c=risk_colors[risk_level],
|
|
245
|
+
alpha=0.8, label=risk_level.capitalize())
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if legend_elements:
|
|
249
|
+
ax.legend(
|
|
250
|
+
handles=legend_elements,
|
|
251
|
+
loc="center left",
|
|
252
|
+
bbox_to_anchor=(1.02, 0.5),
|
|
253
|
+
title="Risk Level",
|
|
254
|
+
fontsize=10,
|
|
255
|
+
title_fontsize=11,
|
|
256
|
+
frameon=False,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# set labels
|
|
260
|
+
xlabel_map = {"quarterly": "Quarter", "semiannual": "Semester", "yearly": "Year"}
|
|
261
|
+
ax.set_xlabel(xlabel_map.get(x_axis_granularity, "Period"), fontsize=13)
|
|
262
|
+
ax.set_ylabel(entity_name, fontsize=13)
|
|
263
|
+
|
|
264
|
+
if title:
|
|
265
|
+
ax.set_title(title, fontsize=14, fontweight="bold", pad=20)
|
|
266
|
+
|
|
267
|
+
ax.tick_params(axis='both', which='major', labelsize=10)
|
|
268
|
+
plt.tight_layout()
|
|
269
|
+
|
|
270
|
+
# save plot if requested
|
|
271
|
+
if save_plot:
|
|
272
|
+
import os
|
|
273
|
+
from datetime import datetime
|
|
274
|
+
|
|
275
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
276
|
+
timestamp = datetime.now().strftime("%Y%m%d")
|
|
277
|
+
granularity_suffix = {
|
|
278
|
+
"quarterly": "Q",
|
|
279
|
+
"yearly": "Y",
|
|
280
|
+
"semiannual": "S",
|
|
281
|
+
}.get(temporal_granularity, "Q")
|
|
282
|
+
plot_path = f"{output_dir}/transitions-{entity_name.lower()}-{granularity_suffix}-{timestamp}.png"
|
|
283
|
+
plt.savefig(plot_path, dpi=300, bbox_inches="tight", format="png")
|
|
284
|
+
print(f"Transition plot saved: {plot_path}")
|
|
285
|
+
|
|
286
|
+
# save CSV if requested
|
|
287
|
+
if save_csv:
|
|
288
|
+
import os
|
|
289
|
+
from datetime import datetime
|
|
290
|
+
|
|
291
|
+
os.makedirs(f"{output_dir}/../results", exist_ok=True)
|
|
292
|
+
timestamp = datetime.now().strftime("%Y%m%d")
|
|
293
|
+
granularity_suffix = {
|
|
294
|
+
"quarterly": "Q",
|
|
295
|
+
"yearly": "Y",
|
|
296
|
+
"semiannual": "S",
|
|
297
|
+
}.get(temporal_granularity, "Q")
|
|
298
|
+
csv_path = f"{output_dir}/../results/transitions-{entity_name.lower()}-{granularity_suffix}-{timestamp}.csv"
|
|
299
|
+
df.to_csv(csv_path, index=False)
|
|
300
|
+
print(f"Transitions CSV saved: {csv_path}")
|
|
301
|
+
|
|
302
|
+
return fig
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Entity movement trajectory plots."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plot_movement_trajectories(
|
|
10
|
+
movement_df: pd.DataFrame,
|
|
11
|
+
entity_name: str = "Entity",
|
|
12
|
+
highlight_entities: Optional[List[str]] = None,
|
|
13
|
+
max_entities: int = 10,
|
|
14
|
+
figsize: Tuple[int, int] = (14, 10),
|
|
15
|
+
title: Optional[str] = None,
|
|
16
|
+
save_plot: bool = True,
|
|
17
|
+
output_dir: str = "plot",
|
|
18
|
+
temporal_granularity: str = "quarterly",
|
|
19
|
+
) -> plt.Figure:
|
|
20
|
+
"""
|
|
21
|
+
Visualize entity trajectories through priority space.
|
|
22
|
+
|
|
23
|
+
Shows how entities move through (X, Y) coordinates over time,
|
|
24
|
+
with arrows indicating direction and markers for start/end points.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
movement_df: DataFrame from track_cumulative_movement()
|
|
28
|
+
Required columns: entity, quarter, period_x, period_y,
|
|
29
|
+
period_quadrant, global_quadrant
|
|
30
|
+
entity_name: Name for entity type (default: "Entity")
|
|
31
|
+
highlight_entities: Specific entities to highlight (default: None = auto-select)
|
|
32
|
+
max_entities: Maximum entities to show (default: 10)
|
|
33
|
+
figsize: Figure size (width, height)
|
|
34
|
+
title: Optional custom title
|
|
35
|
+
save_plot: Save plot to file
|
|
36
|
+
output_dir: Output directory for saved files
|
|
37
|
+
temporal_granularity: Time granularity for file naming
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Matplotlib figure
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
>>> # auto-select top movers
|
|
44
|
+
>>> fig = plot_movement_trajectories(movement_df, entity_name="Service")
|
|
45
|
+
|
|
46
|
+
>>> # highlight specific entities
|
|
47
|
+
>>> fig = plot_movement_trajectories(
|
|
48
|
+
... movement_df,
|
|
49
|
+
... highlight_entities=["Service A", "Service B"],
|
|
50
|
+
... max_entities=5
|
|
51
|
+
... )
|
|
52
|
+
"""
|
|
53
|
+
if movement_df.empty:
|
|
54
|
+
print("No movement data to visualize")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# select entities to plot
|
|
58
|
+
if highlight_entities:
|
|
59
|
+
entities_to_plot = [
|
|
60
|
+
e for e in highlight_entities if e in movement_df["entity"].values
|
|
61
|
+
]
|
|
62
|
+
else:
|
|
63
|
+
# select entities with largest movements
|
|
64
|
+
entity_movement = movement_df.groupby("entity").agg({
|
|
65
|
+
"x_delta": lambda x: abs(x).sum(),
|
|
66
|
+
"y_delta": lambda x: abs(x).sum(),
|
|
67
|
+
})
|
|
68
|
+
entity_movement["total_movement"] = (
|
|
69
|
+
entity_movement["x_delta"] + entity_movement["y_delta"]
|
|
70
|
+
)
|
|
71
|
+
top_movers = entity_movement.nlargest(max_entities, "total_movement")
|
|
72
|
+
entities_to_plot = top_movers.index.tolist()
|
|
73
|
+
|
|
74
|
+
# filter movement data
|
|
75
|
+
df_plot = movement_df[movement_df["entity"].isin(entities_to_plot)].copy()
|
|
76
|
+
|
|
77
|
+
if df_plot.empty:
|
|
78
|
+
print("No entities to plot")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# create figure
|
|
82
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
83
|
+
|
|
84
|
+
# define colors for quadrants
|
|
85
|
+
colors = {
|
|
86
|
+
"Q1": "#c2104b", # red - crisis
|
|
87
|
+
"Q2": "#FF9636", # orange - investigate
|
|
88
|
+
"Q3": "#5cb85c", # green - monitor
|
|
89
|
+
"Q4": "#792f88", # purple - low priority
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# plot trajectories for each entity
|
|
93
|
+
for entity in entities_to_plot:
|
|
94
|
+
entity_data = df_plot[df_plot["entity"] == entity].sort_values("quarter")
|
|
95
|
+
|
|
96
|
+
if len(entity_data) < 2:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# get trajectory coordinates
|
|
100
|
+
x = entity_data["period_x"].values
|
|
101
|
+
y = entity_data["period_y"].values
|
|
102
|
+
|
|
103
|
+
# get color from global quadrant
|
|
104
|
+
global_quad = entity_data.iloc[0]["global_quadrant"]
|
|
105
|
+
color = colors.get(global_quad, "#95a5a6")
|
|
106
|
+
|
|
107
|
+
# plot trajectory line
|
|
108
|
+
ax.plot(x, y, color=color, alpha=0.5, linewidth=2, zorder=1)
|
|
109
|
+
|
|
110
|
+
# add arrows
|
|
111
|
+
for i in range(len(x) - 1):
|
|
112
|
+
dx = x[i + 1] - x[i]
|
|
113
|
+
dy = y[i + 1] - y[i]
|
|
114
|
+
ax.arrow(
|
|
115
|
+
x[i], y[i], dx * 0.9, dy * 0.9,
|
|
116
|
+
head_width=0.1,
|
|
117
|
+
head_length=0.1,
|
|
118
|
+
fc=color,
|
|
119
|
+
ec=color,
|
|
120
|
+
alpha=0.6,
|
|
121
|
+
zorder=2,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# mark start point (circle)
|
|
125
|
+
ax.scatter(
|
|
126
|
+
x[0], y[0],
|
|
127
|
+
s=150,
|
|
128
|
+
c=color,
|
|
129
|
+
marker='o',
|
|
130
|
+
edgecolors='black',
|
|
131
|
+
linewidth=2,
|
|
132
|
+
alpha=0.8,
|
|
133
|
+
zorder=3,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# mark end point (square)
|
|
137
|
+
ax.scatter(
|
|
138
|
+
x[-1], y[-1],
|
|
139
|
+
s=150,
|
|
140
|
+
c=color,
|
|
141
|
+
marker='s',
|
|
142
|
+
edgecolors='black',
|
|
143
|
+
linewidth=2,
|
|
144
|
+
alpha=0.8,
|
|
145
|
+
zorder=3,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# add entity label at end point
|
|
149
|
+
ax.annotate(
|
|
150
|
+
entity,
|
|
151
|
+
(x[-1], y[-1]),
|
|
152
|
+
xytext=(5, 5),
|
|
153
|
+
textcoords="offset points",
|
|
154
|
+
fontsize=10,
|
|
155
|
+
color=color,
|
|
156
|
+
fontweight='bold',
|
|
157
|
+
alpha=0.9,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# add quadrant dividers
|
|
161
|
+
ax.axhline(0, color='grey', linestyle='--', alpha=0.7, linewidth=1, zorder=0)
|
|
162
|
+
ax.axvline(0, color='grey', linestyle='--', alpha=0.7, linewidth=1, zorder=0)
|
|
163
|
+
|
|
164
|
+
# add quadrant labels
|
|
165
|
+
xlim = ax.get_xlim()
|
|
166
|
+
ylim = ax.get_ylim()
|
|
167
|
+
|
|
168
|
+
quadrant_labels = {
|
|
169
|
+
"Q1": ("Q1: Critical", (xlim[1] * 0.9, ylim[1] * 0.9)),
|
|
170
|
+
"Q2": ("Q2: Investigate", (xlim[0] * 0.9, ylim[1] * 0.9)),
|
|
171
|
+
"Q3": ("Q3: Monitor", (xlim[0] * 0.9, ylim[0] * 0.9)),
|
|
172
|
+
"Q4": ("Q4: Low Priority", (xlim[1] * 0.9, ylim[0] * 0.9)),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for label, (x_pos, y_pos) in quadrant_labels.values():
|
|
176
|
+
ax.text(
|
|
177
|
+
x_pos, y_pos,
|
|
178
|
+
label,
|
|
179
|
+
ha="center",
|
|
180
|
+
va="center",
|
|
181
|
+
fontsize=12,
|
|
182
|
+
color="gray",
|
|
183
|
+
alpha=0.3,
|
|
184
|
+
fontweight="bold",
|
|
185
|
+
zorder=0,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# set labels
|
|
189
|
+
ax.set_xlabel(f"{entity_name} Volume (Relative)", fontsize=13)
|
|
190
|
+
ax.set_ylabel(f"{entity_name} Growth Rate (Relative)", fontsize=13)
|
|
191
|
+
|
|
192
|
+
if title:
|
|
193
|
+
ax.set_title(title, fontsize=16, fontweight="bold", pad=20)
|
|
194
|
+
else:
|
|
195
|
+
ax.set_title(f"{entity_name} Movement Trajectories", fontsize=16,
|
|
196
|
+
fontweight="bold", pad=20)
|
|
197
|
+
|
|
198
|
+
# add legend for markers
|
|
199
|
+
from matplotlib.lines import Line2D
|
|
200
|
+
marker_legend = [
|
|
201
|
+
Line2D([0], [0], marker='o', color='w', markerfacecolor='gray',
|
|
202
|
+
markersize=10, label='Start', markeredgecolor='black', markeredgewidth=2),
|
|
203
|
+
Line2D([0], [0], marker='s', color='w', markerfacecolor='gray',
|
|
204
|
+
markersize=10, label='End', markeredgecolor='black', markeredgewidth=2),
|
|
205
|
+
]
|
|
206
|
+
ax.legend(
|
|
207
|
+
handles=marker_legend,
|
|
208
|
+
loc='upper left',
|
|
209
|
+
frameon=False,
|
|
210
|
+
fontsize=10,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
plt.tight_layout()
|
|
214
|
+
|
|
215
|
+
# save plot if requested
|
|
216
|
+
if save_plot:
|
|
217
|
+
import os
|
|
218
|
+
from datetime import datetime
|
|
219
|
+
|
|
220
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
221
|
+
timestamp = datetime.now().strftime("%Y%m%d")
|
|
222
|
+
granularity_suffix = {
|
|
223
|
+
"quarterly": "Q",
|
|
224
|
+
"yearly": "Y",
|
|
225
|
+
"semiannual": "S",
|
|
226
|
+
}.get(temporal_granularity, "Q")
|
|
227
|
+
plot_path = f"{output_dir}/cumulative_movement-{entity_name.lower()}-{granularity_suffix}-{timestamp}.png"
|
|
228
|
+
plt.savefig(plot_path, dpi=300, bbox_inches="tight", format="png")
|
|
229
|
+
print(f"Movement plot saved: {plot_path}")
|
|
230
|
+
|
|
231
|
+
return fig
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: priorityx
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Entity prioritization and escalation detection using GLMM statistical models
|
|
5
|
+
Project-URL: Homepage, https://github.com/okkymabruri/priorityx
|
|
6
|
+
Project-URL: Repository, https://github.com/okkymabruri/priorityx
|
|
7
|
+
Project-URL: Issues, https://github.com/okkymabruri/priorityx/issues
|
|
8
|
+
Author-email: Okky Mabruri <okkymbrur@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: escalation-detection,glmm,prioritization,risk-analysis,statistical-modeling
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: adjusttext>=1.3.0
|
|
24
|
+
Requires-Dist: matplotlib>=3.9.4
|
|
25
|
+
Requires-Dist: pandas>=2.3.3
|
|
26
|
+
Requires-Dist: polars>=1.35.1
|
|
27
|
+
Requires-Dist: pyarrow>=21.0.0
|
|
28
|
+
Requires-Dist: statsmodels>=0.14.5
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Priorityx: Entity prioritization and escalation detection using GLMM statistical models
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install priorityx
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import polars as pl
|
|
43
|
+
from priorityx.core.glmm import fit_priority_matrix
|
|
44
|
+
from priorityx.viz.matrix import plot_priority_matrix
|
|
45
|
+
|
|
46
|
+
df = pl.read_csv("data.csv")
|
|
47
|
+
|
|
48
|
+
results, stats = fit_priority_matrix(
|
|
49
|
+
df,
|
|
50
|
+
entity_col="service",
|
|
51
|
+
timestamp_col="date",
|
|
52
|
+
temporal_granularity="quarterly"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
plot_priority_matrix(results, entity_name="Service", save_plot=True)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- GLMM-based priority classification (Q1-Q4 quadrants)
|
|
61
|
+
- Transition detection over time
|
|
62
|
+
- Movement tracking
|
|
63
|
+
- Visualization
|
|
64
|
+
|
|
65
|
+
## Use Cases
|
|
66
|
+
|
|
67
|
+
IT incidents, software bugs, compliance violations, performance monitoring.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- [Quick Start](docs/quickstart.md)
|
|
72
|
+
- [API Reference](docs/api-reference.md)
|
|
73
|
+
- [Methodology](docs/methodology.md)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
priorityx/__init__.py,sha256=oaQkbWhGBsq0j9arjJdAF14BPe9Jm2dUEyDm3Sgnlko,169
|
|
2
|
+
priorityx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
priorityx/core/filters.py,sha256=RH6bqep8sFIQ346s77Hm-vyJ3Z4lya5qRjXRUcAOXpU,7329
|
|
4
|
+
priorityx/core/glmm.py,sha256=x-qdCdz2Hp4C8FVGO_Or_CBh6n06h2bCFSVaGMyXhNY,13603
|
|
5
|
+
priorityx/core/quadrants.py,sha256=BuHxeD6k-_6GJkbvU59eoW81UzVi1YnMl6evLyf38ks,1910
|
|
6
|
+
priorityx/drivers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
priorityx/tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
priorityx/tracking/drivers.py,sha256=f3-ia36uCFWn4yLDFAqi5re6STmJNXF15luBP1hwH1k,19045
|
|
9
|
+
priorityx/tracking/movement.py,sha256=bqFq0kJjuqxCNkyp1Vr9b2Ed62JVzxaSKOfV7u0iDD0,16580
|
|
10
|
+
priorityx/tracking/transitions.py,sha256=b6r8Bj8i1_N_iUomn-8K162i0k_xdlnNZV48uMKj1Iw,8668
|
|
11
|
+
priorityx/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
priorityx/utils/helpers.py,sha256=yA5mgCCv34MwLSec72PrX1PJY5jt8kN3oZVu6DHKhtQ,7380
|
|
13
|
+
priorityx/viz/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
priorityx/viz/matrix.py,sha256=s5WZ0UWirlau7CpQctVhTNVqd7DXvL5nr3ekZUmuXUs,10413
|
|
15
|
+
priorityx/viz/timeline.py,sha256=-KZ3VM5URRspQbP77HGeLfvV9aEzji8ar2EGN-FxDNo,11039
|
|
16
|
+
priorityx/viz/trajectory.py,sha256=EQ42W5XyFwq7LcCTiWSDZzdW4t5FiDXAWbHD2_K6-Ic,7218
|
|
17
|
+
priorityx-0.1.2.dist-info/METADATA,sha256=bEYIgPOuJWqOZe5XsdaQdro6kTozvA5zCltp5WCOsHg,2167
|
|
18
|
+
priorityx-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
+
priorityx-0.1.2.dist-info/licenses/LICENSE,sha256=653HZgD9wm-uBS8gPWzbTbA4BYPoApwTdRNK-0tbagw,1069
|
|
20
|
+
priorityx-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Okky Mabruri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|