data-transformers について
API ルートやダッシュボードで使うデータ整形処理を集約するスキルです。集計、ランキング、トレンド計算、サニタイズを再利用・テスト可能な変換として扱います。
主な機能
- 01カテゴリ別・件数別集計
- 02Top-N ランキング
- 03期間比較によるトレンド計算
- 04データのサニタイズ
- 05再利用可能な純粋関数
活用シーン
- 01API レスポンス形式の統一
- 02ダッシュボード用集計データの作成
- 03複数ルートでの変換処理の再利用
- 04データ整形ロジックの単体テスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dadbodgeoff/ drift --skill data-transformersインストール後、Claude Code で/data-transformersを入力して読み込まれたか確認します。
詳細
データトランスフォーマー
API ルート全体で一貫してデータを整形するための、集約された変換ロジックです。
このスキルを使う場面
- データ変換が複数のルートに分散している
- エンドポイント間で出力形式を統一したい
- テスト可能で再利用できる変換関数を作りたい
- 集計データを使ったダッシュボードを構築している
主な概念
すべての変換ロジックを一か所に集約します。
- 集計関数(カテゴリ別の合計、件数)
- ランキング関数(スコア順の上位 N 件)
- トレンド計算関数(期間の比較)
- サニタイズ関数(データの検証とクリーンアップ)
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Raw Data │────▶│ Transformers │────▶│ API Output │
└─────────────┘ └──────────────┘ └─────────────┘
実装
TypeScript
// lib/transformers.ts
// ============================================
// Category Aggregation
// ============================================
interface CategoryTotals {
[category: string]: number;
}
function aggregateCategories(
items: Array<{ category: string; count?: number }>
): CategoryTotals {
const totals: CategoryTotals = {};
for (const item of items) {
const category = item.category?.toUpperCase() || 'OTHER';
totals[category] = (totals[category] || 0) + (item.count ?? 1);
}
return totals;
}
function categoriesToBreakdown(
totals: CategoryTotals,
previousTotals?: CategoryTotals
): Array<{ category: string; count: number; percentage: number; trend: string }> {
const total = Object.values(totals).reduce((sum, count) => sum + count, 0);
return Object.entries(totals)
.map(([category, count]) => {
let trend: 'increasing' | 'stable' | 'decreasing' = 'stable';
if (previousTotals) {
const prevCount = previousTotals[category] ?? 0;
const change = count - prevCount;
if (change > prevCount * 0.1) trend = 'increasing';
else if (change < -prevCount * 0.1) trend = 'decreasing';
}
return {
category,
count,
percentage: total > 0 ? count / total : 0,
trend,
};
})
.sort((a, b) => b.count - a.count);
}
// ============================================
// Ranking
// ============================================
interface Rankable {
score: number;
count: number;
}
function rankItems<T extends Rankable>(
items: T[],
limit = 5
): (T & { rank: number })[] {
return items
.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return b.count - a.count;
})
.slice(0, limit)
.map((item, index) => ({ ...item, rank: index + 1 }));
}
// ============================================
// Trend Calculation
// ============================================
type SimpleTrend = 'increasing' | 'stable' | 'decreasing';
function calculateTrend(current: number, previous: number): SimpleTrend {
if (previous === 0) return 'stable';
const change = (current - previous) / previous;
if (change > 0.1) return 'increasing';
if (change < -0.1) return 'decreasing';
return 'stable';
}
function calculateRollingAverage(values: number[], window = 7): number {
if (values.length === 0) return 0;
const slice = values.slice(-window);
return slice.reduce((sum, v) => sum + v, 0) / slice.length;
}
function calculatePercentChange(current: number, previous: number): number {
if (previous === 0) return current > 0 ? 100 : 0;
return ((current - previous) / previous) * 100;
}
// ============================================
// Data Sanitization
// ============================================
interface Hotspot {
country: string;
countryCode: string;
lat: number;
lon: number;
riskScore: number;
eventCount: number;
}
function sanitizeHotspot(raw: Partial<Hotspot>): Hotspot | null {
if (!raw.country || !raw.countryCode) return null;
return {
country: raw.country,
countryCode: raw.countryCode,
lat: raw.lat ?? 0,
lon: raw.lon ?? 0,
riskScore: Math.min(100, Math.max(0, raw.riskScore ?? 0)),
eventCount: Math.max(0, raw.eventCount ?? 0),
};
}
function filterValidHotspots(hotspots: Partial<Hotspot>[]): Hotspot[] {
return hotspots
.map(sanitizeHotspot)
.filter((h): h is Hotspot => h !== null);
}
// ============================================
// String Utilities
// ============================================
function truncate(str: string, maxLen: number): string {
if (!str) return '';
return str.length > maxLen ? str.slice(0, maxLen - 3) + '...' : str;
}
function slugify(str: string): string {
return str
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
// ============================================
// Date Utilities
// ============================================
function formatRelativeTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
export {
aggregateCategories,
categoriesToBreakdown,
rankItems,
calculateTrend,
calculateRollingAverage,
calculatePercentChange,
sanitizeHotspot,
filterValidHotspots,
truncate,
slugify,
formatRelativeTime,
};
使用例
API ルート
// api/dashboard/route.ts
import {
aggregateCategories,
rankItems,
filterValidHotspots
} from '@/lib/transformers';
export async function GET() {
const rawData = await fetchFromDatabase();
return Response.json({
categories: aggregateCategories(rawData.predictions),
topHotspots: rankItems(filterValidHotspots(rawData.hotspots), 5),
trend: calculateTrend(rawData.todayCount, rawData.yesterdayCount),
});
}
ダッシュボードコンポーネント
const breakdown = categoriesToBreakdown(
currentTotals,
previousTotals
);
// Returns:
// [
// { category: 'MILITARY', count: 150, percentage: 0.45, trend: 'increasing' },
// { category: 'POLITICAL', count: 100, percentage: 0.30, trend: 'stable' },
// ...
// ]
ベストプラクティス
- すべてのトランスフォーマーを 1 つのファイルにまとめます。見つけやすく、テストも簡単です
- 純粋関数を使います。副作用がなく、予測可能な出力が得られます
- エッジケースに対応します。空の配列、存在しないフィールド、null 値などを処理します
- 型安全性を確保します。適切な場合は TypeScript のジェネリクスを使います
- 型パッケージからエクスポートします。フロントエンドとバックエンドで共有できます
よくある間違い
- 変換ロジックを複数のルートに分散させる
- エッジケース(空の配列、null 値など)を処理しない
- 新しいオブジェクトを返さずに入力データを変更する
- null 許容の戻り値に型ガードを用意しない
- トランスフォーマーを個別にテストしない
関連パターン
- api-client - API レスポンスでトランスフォーマーを使う
- validation-quarantine - 変換前に検証する
- snapshot-aggregation - ダッシュボード用にデータを集計する
コメント
Markdown 対応