analyze_kpi_across_municipalities
Analyze a Key Performance Indicator across all Swedish municipalities to compare performance statistics, identify top and bottom performers, and benchmark municipal data effectively.
Instructions
Analysera ett KPI över alla kommuner med statistik (min, max, medel, median) och rankning. Visar toppkommuner och bottenkommuner. Perfekt för benchmarking och jämförelser.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| kpi_id | Yes | KPI-ID att analysera (t.ex. "N15033") | |
| year | Yes | År att analysera | |
| gender | No | Kön: T=Totalt, M=Män, K=Kvinnor | T |
| municipality_type | No | Kommuntyp: K=Kommun, L=Region, all=alla | K |
| top_n | No | Antal topprankade att visa (standard: 10) | |
| bottom_n | No | Antal bottenprestanda att visa (standard: 10) |
Implementation Reference
- src/tools/analysis-tools.ts:160-286 (handler)Main tool handler: Fetches municipalities and KPI data from Kolada API in batches (25 IDs max), extracts gender-specific values, computes statistics (min/max/mean/median/std_dev), sorts for top/bottom rankings (top_n/bottom_n), returns structured JSON with analysis.handler: async (args: z.infer<typeof analyzeKpiSchema>): Promise<ToolResult> => { const startTime = Date.now(); const { kpi_id, year, gender, municipality_type, top_n, bottom_n } = args; logger.toolCall('analyze_kpi_across_municipalities', { kpi_id, year, gender, municipality_type, top_n, bottom_n }); try { // Fetch all municipalities const municipalities = await dataCache.getOrFetch( 'municipalities-full', () => koladaClient.fetchAllData<Municipality>('/municipality'), 86400000 ); // Filter by type const filteredMunicipalities = municipality_type === 'all' ? municipalities : municipalities.filter((m) => m.type === municipality_type); // Create ID to name mapping const idToName: Record<string, string> = {}; filteredMunicipalities.forEach((m) => { idToName[m.id] = m.title; }); // Fetch KPI data in batches (max 25 municipalities per request to avoid URL length issues) const BATCH_SIZE = 25; const municipalityIdChunks = chunkArray(filteredMunicipalities.map((m) => m.id), BATCH_SIZE); const allData: KPIData[] = []; for (const chunk of municipalityIdChunks) { const endpoint = `/data/kpi/${kpi_id}/municipality/${chunk.join(',')}/year/${year}`; const batchData = await koladaClient.fetchAllData<KPIData>(endpoint); allData.push(...batchData); } const data = allData; // Extract values with municipality info const municipalityValues: { id: string; name: string; value: number }[] = []; for (const dataPoint of data) { if (!dataPoint.municipality) continue; const value = extractValue(dataPoint, gender); if (value !== null) { municipalityValues.push({ id: dataPoint.municipality, name: idToName[dataPoint.municipality] || dataPoint.municipality, value, }); } } if (municipalityValues.length === 0) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'NO_DATA', message: `Ingen data hittades för KPI ${kpi_id}, år ${year}, kön ${gender}`, suggestion: 'Försök med ett annat år eller kontrollera att KPI-ID:t är korrekt.', }), }, ], isError: true, }; } // Calculate statistics const values = municipalityValues.map((mv) => mv.value); const stats = calculateStats(values); // Sort for rankings const sorted = [...municipalityValues].sort((a, b) => b.value - a.value); const topMunicipalities = sorted.slice(0, top_n); const bottomMunicipalities = sorted.slice(-bottom_n).reverse(); // Calculate how many are above/below average const aboveAverage = municipalityValues.filter((mv) => mv.value > stats.mean).length; const belowAverage = municipalityValues.filter((mv) => mv.value < stats.mean).length; logger.toolResult('analyze_kpi_across_municipalities', true, Date.now() - startTime); return { content: [ { type: 'text', text: JSON.stringify( { kpi_id, year, gender, municipality_type, statistics: { count: stats.count, min: parseFloat(stats.min.toFixed(2)), max: parseFloat(stats.max.toFixed(2)), mean: parseFloat(stats.mean.toFixed(2)), median: parseFloat(stats.median.toFixed(2)), std_dev: parseFloat(stats.std_dev.toFixed(2)), above_average: aboveAverage, below_average: belowAverage, }, top_municipalities: topMunicipalities.map((m, i) => ({ rank: i + 1, id: m.id, name: m.name, value: parseFloat(m.value.toFixed(2)), })), bottom_municipalities: bottomMunicipalities.map((m, i) => ({ rank: stats.count - bottom_n + i + 1, id: m.id, name: m.name, value: parseFloat(m.value.toFixed(2)), })), source: 'Kolada - Källa: Kolada', }, null, 2 ), }, ], }; } catch (error) { logger.toolResult('analyze_kpi_across_municipalities', false, Date.now() - startTime); throw error; } },
- src/tools/analysis-tools.ts:25-32 (schema)Zod input schema validating and describing tool parameters with defaults and constraints.const analyzeKpiSchema = z.object({ kpi_id: z.string().describe('KPI-ID att analysera (t.ex. "N15033")'), year: z.number().describe('År att analysera'), gender: z.enum(['T', 'M', 'K']).default('T').describe('Kön: T=Totalt, M=Män, K=Kvinnor'), municipality_type: z.enum(['K', 'L', 'all']).default('K').describe('Kommuntyp: K=Kommun, L=Region, all=alla'), top_n: z.number().min(1).max(50).default(10).describe('Antal topprankade att visa (standard: 10)'), bottom_n: z.number().min(1).max(50).default(10).describe('Antal bottenprestanda att visa (standard: 10)'), });
- src/server/handlers.ts:32-38 (registration)Tool registry: analysisTools object spread into central allTools export, used by MCP server handlers for tool listing (ListToolsRequestSchema) and execution (CallToolRequestSchema).export const allTools = { ...kpiTools, ...municipalityTools, ...ouTools, ...dataTools, ...analysisTools, };
- src/tools/analysis-tools.ts:85-113 (helper)Helper function to compute descriptive statistics (count, min, max, mean, median, standard deviation) for KPI values across municipalities.function calculateStats(values: number[]): { count: number; min: number; max: number; mean: number; median: number; std_dev: number; } { if (values.length === 0) { return { count: 0, min: NaN, max: NaN, mean: NaN, median: NaN, std_dev: NaN }; } const sorted = [...values].sort((a, b) => a - b); const count = values.length; const min = sorted[0]; const max = sorted[count - 1]; const mean = values.reduce((a, b) => a + b, 0) / count; // Median const mid = Math.floor(count / 2); const median = count % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; // Standard deviation const squaredDiffs = values.map((v) => Math.pow(v - mean, 2)); const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / count; const std_dev = Math.sqrt(avgSquaredDiff); return { count, min, max, mean, median, std_dev }; }
- src/tools/analysis-tools.ts:118-134 (helper)Helper to extract numeric value for specific gender (T/M/K) from KPIData point, handling missing data gracefully.function extractValue(dataPoint: KPIData, gender: 'T' | 'M' | 'K'): number | null { if (!dataPoint.values || dataPoint.values.length === 0) return null; // Try to find gender-specific value const genderValue = dataPoint.values.find((v) => v.gender === gender); if (genderValue && genderValue.value !== null && genderValue.value !== undefined) { return genderValue.value; } // Fall back to first value if no gender match const firstValue = dataPoint.values[0]; if (firstValue && firstValue.value !== null && firstValue.value !== undefined) { return firstValue.value; } return null; }