Skip to content

Commit eb151c2

Browse files
committed
Add data processing scripts for ticket analysis and routing
- Implemented _check_fields.py to analyze fields in the CSV data and print useful content. - Created _check_more_fields.py to explore additional fields of interest and their fill rates. - Developed _curate_data.py to curate and score tickets for training, focusing on informative content. - Added _import_csv.py to import CSV data, analyze incident types, and generate new tickets for underrepresented groups. - Introduced _predict_group.py to evaluate predictive power of various fields for ticket assignment. - Built _rebuild_data.py to create a balanced dataset for ticket routing, ensuring representation across groups.
1 parent 2bc6b39 commit eb151c2

6 files changed

Lines changed: 535 additions & 0 deletions

File tree

notebooks/_check_fields.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import csv
2+
3+
with open('/Users/abossard/Desktop/projects/python-quart-vite-react/csv/data.csv', encoding='utf-8', errors='replace') as f:
4+
reader = csv.DictReader(f)
5+
rows = list(reader)
6+
7+
# Check which fields have useful content for the first ticket
8+
sample = rows[0]
9+
useful_fields = []
10+
for key, val in sample.items():
11+
val = str(val).strip()
12+
if val and len(val) > 5 and len(val) < 500:
13+
useful_fields.append((key, val[:120]))
14+
15+
print(f"Fields with content (5-500 chars) in first row:")
16+
for key, val in useful_fields[:40]:
17+
print(f" {key:50s} | {val}")
18+
19+
# Check Notes and Resolution fields specifically
20+
print(f"\n━━━ Key content fields ━━━")
21+
for field in ['Summary*', 'Notes', 'Resolution', 'Operational Categorization Tier 1+',
22+
'Operational Categorization Tier 2', 'Operational Categorization Tier 3',
23+
'Product Categorization Tier 1', 'Product Categorization Tier 2',
24+
'Product Categorization Tier 3', 'Service*+', 'CI+', 'Incident Type*',
25+
'Category', 'Short Description']:
26+
val = str(sample.get(field, '')).strip()
27+
print(f" {field:50s} | {val[:150]}")
28+
29+
# Check a few more rows for Notes content
30+
print(f"\n━━━ Notes samples ━━━")
31+
for r in rows[:5]:
32+
summary = str(r.get('Summary*', '')).strip()[:40]
33+
notes = str(r.get('Notes', '')).strip()[:100]
34+
resolution = str(r.get('Resolution', '')).strip()[:100]
35+
short_desc = str(r.get('Short Description', '')).strip()[:100]
36+
op_cat1 = str(r.get('Operational Categorization Tier 1+', '')).strip()
37+
op_cat2 = str(r.get('Operational Categorization Tier 2', '')).strip()
38+
service = str(r.get('Service*+', '')).strip()[:60]
39+
product = str(r.get('Product Categorization Tier 1', '')).strip()
40+
print(f"\n Summary: {summary}")
41+
print(f" Notes: {notes}")
42+
print(f" Resolution: {resolution}")
43+
print(f" Short Desc: {short_desc}")
44+
print(f" OpCat: {op_cat1} > {op_cat2}")
45+
print(f" Service: {service}")
46+
print(f" Product: {product}")

notebooks/_check_more_fields.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import csv
2+
from collections import Counter
3+
4+
with open('/Users/abossard/Desktop/projects/python-quart-vite-react/csv/data.csv', encoding='utf-8', errors='replace') as f:
5+
reader = csv.DictReader(f)
6+
rows = list(reader)
7+
8+
# Fields we already use: Summary*, Notes, Service*+, Product Categorization Tier 3, Priority*, Assigned Group*+, Incident Type*
9+
# What else could be interesting?
10+
11+
interesting = [
12+
# Organization context
13+
('Company', 'Which department/org filed it'),
14+
('Organization', 'Sub-org of the requester'),
15+
('Support Organization', 'Support org handling it'),
16+
17+
# Categorization
18+
('Operational Categorization Tier 1+', 'OpCat level 1'),
19+
('Operational Categorization Tier 2', 'OpCat level 2'),
20+
('Operational Categorization Tier 3', 'OpCat level 3'),
21+
('Product Categorization Tier 1', 'Product level 1'),
22+
('Product Categorization Tier 2', 'Product level 2'),
23+
('Product Categorization Tier 3', 'Product level 3'),
24+
25+
# Service info
26+
('Service*+', 'Service name'),
27+
('CI+', 'Configuration Item'),
28+
('CI Name', 'CI readable name'),
29+
30+
# Resolution (could teach what worked)
31+
('Resolution', 'How it was resolved'),
32+
('Resolution Categorization Tier 1', 'Resolution cat'),
33+
34+
# Impact/urgency
35+
('Urgency*', 'How urgent'),
36+
('Impact*', 'How impactful'),
37+
38+
# Source
39+
('Reported Source', 'How was it reported'),
40+
41+
# Status
42+
('Status*', 'Current status'),
43+
('Status Reason', 'Why this status'),
44+
]
45+
46+
print("Field analysis (first 20 rows sampled):\n")
47+
for field, desc in interesting:
48+
values = [str(r.get(field, '')).strip() for r in rows[:100] if str(r.get(field, '')).strip()]
49+
unique = len(set(values))
50+
if values:
51+
top = Counter(values).most_common(3)
52+
sample = values[0][:60]
53+
fill_rate = len(values) / min(100, len(rows))
54+
print(f" {field:45s} | {fill_rate:3.0%} filled | {unique:3d} unique | {desc}")
55+
print(f" Top: {", ".join(f"{v[:30]}({c})" for v,c in top)}")
56+
print()

notebooks/_curate_data.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import csv, json
2+
from collections import Counter
3+
4+
with open('/Users/abossard/Desktop/projects/python-quart-vite-react/csv/data.csv', encoding='utf-8', errors='replace') as f:
5+
reader = csv.DictReader(f)
6+
rows = list(reader)
7+
8+
TYPE_MAP = {
9+
'User Service Request': 'Service Request',
10+
'Infrastructure Event': 'Event NO Customer Impact',
11+
'User Service Restoration': 'Failure',
12+
}
13+
14+
TARGET_GROUPS = [
15+
'SDE - Service Desk', 'OFC - Office & Collaboration', 'PRM - Premium Support',
16+
'CDC - Client Design & Standard SW Integration', 'OUM - Incident', 'Helpdesk',
17+
'Service-Center IKT', 'Service-Center IKT Bestellungen', 'Smartcard Office',
18+
'CBCD - Container Basierte Cloud Dienste', 'OPC - Applikationen', 'DevOps - Rein',
19+
'BVX - ePortal Service Line', 'IOM - Input / Output Mgmt.', 'OPM - DLC Dispatching',
20+
'O-SDK', 'ESTV-RSS-Stammdaten', 'FIB - BIT Store Bollwerk', 'Immobilien BAZG',
21+
'Bedarfsmanagement', 'Güter und Ausrüstung',
22+
]
23+
24+
def build_summary(row):
25+
"""Build a concise but informative summary."""
26+
parts = []
27+
summary = str(row.get('Summary*', '')).strip()
28+
if summary:
29+
parts.append(summary)
30+
31+
# Add the key detail from notes (not the whole blob)
32+
notes = str(row.get('Notes', '')).strip()
33+
if notes and summary and notes.startswith(summary):
34+
notes = notes[len(summary):].strip()
35+
if notes and len(notes) > 5:
36+
# Take first sentence/clause only
37+
for sep in ['. ', ' ', '\n']:
38+
if sep in notes:
39+
notes = notes[:notes.index(sep)]
40+
break
41+
if len(notes) > 120:
42+
notes = notes[:120] + '...'
43+
if notes:
44+
parts.append(notes)
45+
46+
# Company (strong routing signal — BAZG tickets go to specific groups)
47+
company = str(row.get('Company', '')).strip()
48+
if company and len(company) > 2:
49+
parts.append(f'[Firma: {company}]')
50+
51+
# Service name (strongest predictor — 85% uncertainty reduction)
52+
service = str(row.get('Service*+', '')).strip()
53+
if service and len(service) > 2:
54+
parts.append(f'[Service: {service}]')
55+
56+
# Product domain (Workplace vs ESTV vs Customer facing)
57+
prod2 = str(row.get('Product Categorization Tier 2', '')).strip()
58+
if prod2 and len(prod2) > 2:
59+
parts.append(f'[Bereich: {prod2}]')
60+
61+
return ' — '.join(parts) if len(parts) > 1 else (parts[0] if parts else '')
62+
63+
def score_ticket(row):
64+
"""Score how informative/interesting a ticket is for training."""
65+
score = 0
66+
notes = str(row.get('Notes', '')).strip()
67+
summary = str(row.get('Summary*', '')).strip()
68+
service = str(row.get('Service*+', '')).strip()
69+
70+
if len(notes) > 20: score += 2 # Has real description
71+
if len(summary) > 10: score += 1 # Descriptive title
72+
if service: score += 1 # Has service context
73+
if any(kw in notes.lower() for kw in ['fehler', 'problem', 'kann nicht', 'geht nicht', 'funktioniert nicht', 'störung', 'ausfall', 'gesperrt', 'defekt']):
74+
score += 2 # Has error keywords
75+
if any(kw in notes.lower() for kw in ['bestell', 'anfrage', 'freischalt', 'zugang', 'berechtigung', 'neu', 'install']):
76+
score += 1 # Has request keywords
77+
return score
78+
79+
# Collect and score all matching tickets
80+
all_tickets = []
81+
for row in rows:
82+
group = str(row.get('Assigned Group*+', '')).strip()
83+
if group not in TARGET_GROUPS:
84+
continue
85+
86+
rich_summary = build_summary(row)
87+
if not rich_summary or len(rich_summary) < 8:
88+
continue
89+
90+
# Use Operational Categorization Tier 1 as the real category (100% filled, accurate)
91+
op_cat = str(row.get('Operational Categorization Tier 1+', '')).strip()
92+
if op_cat in ('Service Request', 'Failure', 'Event NO Customer Impact'):
93+
category = op_cat
94+
else:
95+
inc_type = str(row.get('Incident Type*', '')).strip()
96+
category = TYPE_MAP.get(inc_type, 'Service Request')
97+
priority = str(row.get('Priority*', 'Standard')).strip()
98+
if priority not in ('Standard', 'Medium', 'High'):
99+
priority = 'Standard'
100+
101+
all_tickets.append({
102+
'summary': rich_summary,
103+
'category': category,
104+
'priority': priority,
105+
'assigned_group': group,
106+
'_score': score_ticket(row),
107+
})
108+
109+
# Pick the BEST tickets per group: 4 per group max, sorted by score
110+
final_tickets = []
111+
seen = set()
112+
for group in TARGET_GROUPS:
113+
group_tickets = sorted(
114+
[t for t in all_tickets if t['assigned_group'] == group],
115+
key=lambda t: t['_score'],
116+
reverse=True
117+
)
118+
count = 0
119+
for t in group_tickets:
120+
if count >= 4:
121+
break
122+
# Skip near-duplicates
123+
short = t['summary'][:30]
124+
if short in seen:
125+
continue
126+
seen.add(short)
127+
ticket = {k: v for k, v in t.items() if not k.startswith('_')}
128+
final_tickets.append(ticket)
129+
count += 1
130+
131+
with open('datasets/ticket_routing.json', 'w') as f:
132+
json.dump(final_tickets, f, indent=2, ensure_ascii=False)
133+
134+
print(f'Total: {len(final_tickets)} curated tickets')
135+
final_groups = Counter(t['assigned_group'] for t in final_tickets)
136+
for g, c in sorted(final_groups.items(), key=lambda x: -x[1]):
137+
print(f' {c} {g}')
138+
139+
print(f'\n━━━ Samples ━━━')
140+
import random
141+
for t in random.sample(final_tickets, min(8, len(final_tickets))):
142+
print(f'\n [{t["assigned_group"]}] {t["category"]} / {t["priority"]}')
143+
print(f' {t["summary"][:140]}')

notebooks/_import_csv.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import csv, json
2+
from collections import Counter
3+
4+
with open('/Users/abossard/Desktop/projects/python-quart-vite-react/csv/data.csv', encoding='utf-8', errors='replace') as f:
5+
reader = csv.DictReader(f)
6+
rows = list(reader)
7+
8+
# Check Incident Type as category proxy
9+
inc_types = Counter(r.get('Incident Type*', '') for r in rows)
10+
print(f'Incident Types: {dict(inc_types.most_common(10))}')
11+
12+
# Map Incident Type to our categories
13+
TYPE_MAP = {
14+
'User Service Request': 'Service Request',
15+
'Infrastructure Event': 'Event NO Customer Impact',
16+
'User Service Restoration': 'Failure',
17+
}
18+
19+
# Get the groups that exist in our current dataset
20+
with open('datasets/ticket_routing.json') as f:
21+
existing = json.load(f)
22+
existing_groups = set(d['assigned_group'] for d in existing)
23+
print(f'\nExisting groups in JSON: {len(existing_groups)}')
24+
25+
# Find CSV rows that match our groups
26+
matching = [r for r in rows if r.get('Assigned Group*+', '').strip() in existing_groups]
27+
print(f'CSV rows matching existing groups: {len(matching)}')
28+
29+
# For each group, count how many CSV rows we have
30+
groups = Counter(r.get('Assigned Group*+', '').strip() for r in matching)
31+
print(f'\nCSV data per existing group:')
32+
for g in sorted(existing_groups):
33+
csv_count = groups.get(g, 0)
34+
existing_count = sum(1 for d in existing if d['assigned_group'] == g)
35+
print(f' {csv_count:4d} CSV | {existing_count:2d} JSON | {g}')
36+
37+
# Generate new JSON from CSV - take up to 5 per group for underrepresented ones
38+
new_tickets = []
39+
for group in existing_groups:
40+
group_rows = [r for r in rows if r.get('Assigned Group*+', '').strip() == group]
41+
existing_count = sum(1 for d in existing if d['assigned_group'] == group)
42+
need = max(0, 5 - existing_count) # Want at least 5 per group
43+
44+
for r in group_rows[:need]:
45+
summary = r.get('Summary*', '').strip()
46+
if not summary or len(summary) < 3:
47+
continue
48+
inc_type = r.get('Incident Type*', '').strip()
49+
category = TYPE_MAP.get(inc_type, 'Service Request')
50+
priority = r.get('Priority*', 'Standard').strip()
51+
if priority not in ('Standard', 'Medium', 'High'):
52+
priority = 'Standard'
53+
54+
# Skip if already in existing
55+
if any(d['summary'] == summary for d in existing):
56+
continue
57+
58+
new_tickets.append({
59+
'summary': summary,
60+
'category': category,
61+
'priority': priority,
62+
'assigned_group': group,
63+
})
64+
65+
print(f'\nNew tickets to add: {len(new_tickets)}')
66+
for t in new_tickets[:10]:
67+
print(f' {t["assigned_group"]:40s} | {t["category"]:20s} | {t["priority"]:10s} | {t["summary"][:50]}')
68+
if len(new_tickets) > 10:
69+
print(f' ... and {len(new_tickets) - 10} more')
70+
71+
# Write updated dataset
72+
existing.extend(new_tickets)
73+
with open('datasets/ticket_routing.json', 'w') as f:
74+
json.dump(existing, f, indent=2, ensure_ascii=False)
75+
print(f'\nUpdated dataset: {len(existing)} tickets (was {len(existing) - len(new_tickets)})')
76+
77+
# Final stats
78+
final_groups = Counter(d['assigned_group'] for d in existing)
79+
single = sum(1 for c in final_groups.values() if c == 1)
80+
print(f'Groups with only 1 example: {single} (was 14)')

0 commit comments

Comments
 (0)