Skip to main content
Glama

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
NameRequiredDescriptionDefault
kpi_idYesKPI-ID att analysera (t.ex. "N15033")
yearYesÅr att analysera
genderNoKön: T=Totalt, M=Män, K=KvinnorT
municipality_typeNoKommuntyp: K=Kommun, L=Region, all=allaK
top_nNoAntal topprankade att visa (standard: 10)
bottom_nNoAntal bottenprestanda att visa (standard: 10)

Implementation Reference

  • 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; } },
  • 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)'), });
  • 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, };
  • 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 }; }
  • 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; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/isakskogstad/Kolada-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server