Lodash で巨大 JSON を“正規化 → 集計 → 整形”する 7 ステップ実装

実際のアプリケーション開発では、API から受け取った複雑な JSON データを、そのまま利用できることは稀です。巨大で複雑なネストした JSON を、まず正規化してフラットにし、次に集計処理を行い、最後にビューに最適な形に整形する――この一連の流れを Lodash を使って実装できれば、開発効率が劇的に向上します。
本記事では、Lodash の各種関数を駆使して、巨大 JSON データを「正規化 → 集計 → 整形」する 7 ステップの実装パターンを、豊富なサンプルコードとともに詳しく解説いたします。
背景
API レスポンスに潜む複雑なデータ構造の課題
現代の Web アプリケーション開発では、REST API や GraphQL から取得する JSON データは、非常に複雑な構造を持つことが一般的です。
例えば、EC サイトの注文データや、CRM システムの顧客情報など、実際のビジネスデータは多くの場合、以下のような特徴を持っています。
javascript// APIから取得した生のJSONデータの例
const rawApiResponse = {
status: 'success',
data: {
orders: [
{
orderId: 'ORD-001',
customer: {
id: 'CUST-123',
profile: {
name: '田中太郎',
contact: {
email: 'tanaka@example.com',
phone: '090-1234-5678',
},
address: {
prefecture: '東京都',
city: '渋谷区',
street: '神南1-2-3',
},
},
tier: 'premium',
},
items: [
{
productId: 'PROD-A001',
name: 'ワイヤレスマウス',
category: { main: '電子機器', sub: '周辺機器' },
price: 3500,
quantity: 2,
discount: { type: 'percentage', value: 10 },
},
{
productId: 'PROD-B002',
name: 'USB充電ケーブル',
category: { main: '電子機器', sub: 'ケーブル' },
price: 1200,
quantity: 3,
discount: { type: 'fixed', value: 200 },
},
],
payment: {
method: 'credit_card',
status: 'completed',
timestamp: '2024-01-15T10:30:00Z',
},
shipping: {
method: 'express',
status: 'pending',
estimatedDate: '2024-01-17',
},
},
// ... 数百〜数千件のデータ
],
metadata: {
total: 1523,
page: 1,
perPage: 50,
},
},
};
このような複雑にネストした構造を持つ JSON データには、以下のような課題があります。
以下の図は、複雑な JSON データの構造と課題を示しています。
mermaidflowchart TD
A["API Response"] --> B["深いネスト構造"]
A --> C["配列内のオブジェクト"]
A --> D["重複データの混在"]
B --> E["アクセスが煩雑"]
C --> F["集計処理が複雑"]
D --> G["データ整合性の問題"]
E --> H["開発効率の低下"]
F --> H
G --> H
図で理解できる要点
- API レスポンスには深いネスト、配列内オブジェクト、重複データなど複数の課題が存在
- これらの課題は開発効率の低下に直結する
- 適切なデータ変換処理が必要不可欠
データ正規化の必要性とメリット
データベース設計における正規化と同様に、JSON データも正規化することで、以下のようなメリットが得られます。
# | メリット | 詳細 |
---|---|---|
1 | アクセスの簡素化 | ネストを浅くし、データアクセスが容易に |
2 | 重複の排除 | 同じデータの重複を避け、一貫性を保つ |
3 | 集計処理の効率化 | フラットなデータ構造により集計が高速化 |
4 | メモリ効率の向上 | 重複データの削減によりメモリ使用量を削減 |
5 | 更新処理の単純化 | データの更新箇所が一箇所に集約される |
正規化されたデータ構造の例を見てみましょう。
javascript// 正規化後のデータ構造イメージ
const normalizedData = {
customers: {
'CUST-123': {
id: 'CUST-123',
name: '田中太郎',
email: 'tanaka@example.com',
tier: 'premium',
// ... その他の顧客情報
},
},
products: {
'PROD-A001': {
id: 'PROD-A001',
name: 'ワイヤレスマウス',
category: '電子機器',
subCategory: '周辺機器',
basePrice: 3500,
},
'PROD-B002': {
id: 'PROD-B002',
name: 'USB充電ケーブル',
category: '電子機器',
subCategory: 'ケーブル',
basePrice: 1200,
},
},
orders: {
'ORD-001': {
id: 'ORD-001',
customerId: 'CUST-123',
items: ['ITEM-001', 'ITEM-002'],
paymentStatus: 'completed',
shippingStatus: 'pending',
},
},
orderItems: {
'ITEM-001': {
id: 'ITEM-001',
orderId: 'ORD-001',
productId: 'PROD-A001',
quantity: 2,
discountType: 'percentage',
discountValue: 10,
},
'ITEM-002': {
id: 'ITEM-002',
orderId: 'ORD-001',
productId: 'PROD-B002',
quantity: 3,
discountType: 'fixed',
discountValue: 200,
},
},
};
Lodash を活用した効率的なデータ変換の優位性
生の JavaScript でこれらのデータ変換処理を実装しようとすると、以下のような問題が発生します。
javascript// 生のJavaScriptでの実装例(冗長で読みにくい)
function normalizeOrders(rawData) {
const normalized = {
customers: {},
products: {},
orders: {},
orderItems: {},
};
// ネストしたループと条件分岐が複雑に絡み合う
rawData.data.orders.forEach((order) => {
// 顧客データの抽出
if (!normalized.customers[order.customer.id]) {
normalized.customers[order.customer.id] = {
id: order.customer.id,
name: order.customer.profile.name,
email: order.customer.profile.contact.email,
// ... 深いネストのアクセス
};
}
// 商品データの抽出
order.items.forEach((item, index) => {
if (!normalized.products[item.productId]) {
normalized.products[item.productId] = {
id: item.productId,
name: item.name,
// ...
};
}
// 注文アイテムの登録
const itemId = `ITEM-${order.orderId}-${index}`;
normalized.orderItems[itemId] = {
// ...
};
});
// さらにコードが続く...
});
return normalized;
}
このコードには以下のような課題があります:
- コードが非常に長く、可読性が低い
- ネストしたデータアクセスが煩雑で、エラーが発生しやすい
- null や undefined のチェックが煩雑
- 保守性が低く、後から修正が困難
一方、Lodash を使うことで、これらの処理を簡潔で読みやすく実装できます。
javascriptimport {
flatMap,
keyBy,
mapValues,
pick,
get,
} from 'lodash';
// Lodashを使った簡潔な実装
function normalizeOrdersWithLodash(rawData) {
const orders = rawData.data.orders;
// 顧客データを正規化(1行で完結)
const customers = keyBy(
orders.map((order) => ({
id: get(order, 'customer.id'),
name: get(order, 'customer.profile.name'),
email: get(order, 'customer.profile.contact.email'),
tier: get(order, 'customer.tier'),
})),
'id'
);
// 商品データを正規化
const products = keyBy(
flatMap(orders, (order) =>
order.items.map((item) => ({
id: item.productId,
name: item.name,
category: get(item, 'category.main'),
subCategory: get(item, 'category.sub'),
basePrice: item.price,
}))
),
'id'
);
return { customers, products };
}
Lodash を活用することで、以下のメリットが得られるのです:
- コードが簡潔で読みやすい
- 安全な null チェックが組み込まれている(
get
関数) - 関数型プログラミングの手法により保守性が向上
- チーム開発での理解が容易
課題
巨大 JSON の処理におけるパフォーマンス問題
実際のプロダクション環境では、数千〜数万件のレコードを含む巨大な JSON データを扱うことも珍しくありません。
以下の表は、データ規模によるパフォーマンス課題を整理したものです。
# | データ件数 | 処理時間(生 JS) | メモリ使用量 | 主な課題 |
---|---|---|---|---|
1 | 100 件 | 10ms | 2MB | 特になし |
2 | 1,000 件 | 80ms | 15MB | 実装の複雑さ |
3 | 10,000 件 | 850ms | 120MB | UI の応答性低下 |
4 | 100,000 件 | 9,500ms | 1.2GB | ブラウザのフリーズリスク |
5 | 1,000,000 件 | 120,000ms | 12GB | 処理が実質的に不可能 |
大量のデータを扱う際の主な課題は以下の通りです:
javascript// パフォーマンス問題の例
function processLargeData(data) {
// 問題1: 多重ループによる処理時間の増大
const result = data.map((item) => {
return item.children.map((child) => {
return child.items.filter((i) => i.active);
});
});
// 問題2: 不要なデータのコピーによるメモリ圧迫
const copied = JSON.parse(JSON.stringify(data));
// 問題3: 非効率な検索処理の繰り返し
const enriched = data.map((item) => {
const category = categories.find(
(c) => c.id === item.categoryId
);
return { ...item, category };
});
return result;
}
データ集計における条件分岐の複雑化
ビジネスロジックに基づいたデータ集計では、様々な条件での集計が必要になります。
以下の図は、集計処理の複雑さを示しています。
mermaidflowchart TD
A["生データ"] --> B{"集計軸の選択"}
B -->|顧客属性別| C["年齢層・地域・会員ランク"]
B -->|商品属性別| D["カテゴリ・価格帯・ブランド"]
B -->|時系列別| E["日・週・月・四半期"]
B -->|複合条件| F["複数軸の組み合わせ"]
C --> G["集計値の計算"]
D --> G
E --> G
F --> G
G --> H["合計・平均・最大・最小"]
G --> I["カウント・比率"]
G --> J["カスタム計算"]
図で理解できる要点
- 集計には複数の軸(顧客・商品・時系列など)が存在
- 各軸ごとに異なる集計値の計算が必要
- 複合条件の場合、さらに複雑性が増す
従来の実装では、これらの集計処理が非常に複雑になってしまいます。
javascript// 複雑な集計処理の例
function aggregateOrderData(orders) {
const stats = {
byCustomerTier: {},
byCategory: {},
byDate: {},
};
orders.forEach((order) => {
const tier = order.customer.tier;
if (!stats.byCustomerTier[tier]) {
stats.byCustomerTier[tier] = {
count: 0,
total: 0,
items: [],
};
}
order.items.forEach((item) => {
const category = item.category.main;
if (!stats.byCategory[category]) {
stats.byCategory[category] = {
count: 0,
total: 0,
items: [],
};
}
// さらにネストした集計処理が続く...
});
});
return stats;
}
UI 表示に最適なデータ形式への整形の困難さ
最終的に、集計したデータを UI コンポーネントで表示するためには、フレームワークやライブラリが要求する特定の形式に整形する必要があります。
例えば、以下のような形式変換が必要になることがあります:
javascript// Chart.jsで必要なデータ形式
const chartData = {
labels: ['1月', '2月', '3月'],
datasets: [
{
label: '売上',
data: [120000, 150000, 180000],
backgroundColor: 'rgba(75, 192, 192, 0.2)',
},
],
};
// テーブルコンポーネントで必要なデータ形式
const tableData = [
{
id: 1,
name: '商品A',
category: '電子機器',
sales: 120000,
},
// ...
];
// セレクトボックスで必要なデータ形式
const selectOptions = [
{ value: 'cat1', label: '電子機器' },
{ value: 'cat2', label: '衣料品' },
// ...
];
これらの変換処理を手動で実装すると、コードが冗長になり、保守性が低下してしまいます。
解決策
Lodash の各種関数を組み合わせることで、「正規化 → 集計 → 整形」の一連のデータ処理を、7 つのステップとして体系的に実装できます。
以下の図は、7 ステップの全体像を示しています。
mermaidflowchart LR
A["Step1<br/>正規化"] --> B["Step2<br/>重複排除"]
B --> C["Step3<br/>キー変換"]
C --> D["Step4<br/>グルーピング"]
D --> E["Step5<br/>集計"]
E --> F["Step6<br/>ソート"]
F --> G["Step7<br/>整形"]
G --> H["UI表示用データ"]
図で理解できる要点
- データ処理は 7 つの明確なステップに分解できる
- 各ステップは独立しており、組み合わせが柔軟
- 最終的に UI 表示に最適な形式になる
それでは、各ステップの実装方法を詳しく見ていきましょう。
Step 1: flatMap と get でネストを正規化
ネストした JSON データをフラットな構造に変換するには、flatMap
と get
を組み合わせます。
flatMap
は、配列の各要素をマッピングした後、結果を 1 階層フラットにする関数です。get
は、安全にネストしたプロパティにアクセスできる関数です。
javascriptimport { flatMap, get } from 'lodash';
// ネストしたAPIレスポンス
const apiResponse = {
data: {
orders: [
{
orderId: 'ORD-001',
customer: {
id: 'CUST-123',
profile: {
name: '田中太郎',
contact: { email: 'tanaka@example.com' },
},
},
items: [
{
productId: 'PROD-A001',
name: 'ワイヤレスマウス',
price: 3500,
quantity: 2,
},
{
productId: 'PROD-B002',
name: 'USB充電ケーブル',
price: 1200,
quantity: 3,
},
],
},
{
orderId: 'ORD-002',
customer: {
id: 'CUST-456',
profile: {
name: '佐藤花子',
contact: { email: 'sato@example.com' },
},
},
items: [
{
productId: 'PROD-C003',
name: 'ノートPC',
price: 120000,
quantity: 1,
},
],
},
],
},
};
この複雑にネストしたデータを、注文アイテム単位でフラットに正規化します。
javascript// Step 1: 正規化処理
const normalizedItems = flatMap(
apiResponse.data.orders,
(order) => {
return order.items.map((item) => ({
// 注文情報
orderId: order.orderId,
// 顧客情報(安全にネストアクセス)
customerId: get(order, 'customer.id'),
customerName: get(order, 'customer.profile.name'),
customerEmail: get(
order,
'customer.profile.contact.email'
),
// 商品情報
productId: item.productId,
productName: item.name,
price: item.price,
quantity: item.quantity,
// 計算フィールド
totalPrice: item.price * item.quantity,
}));
}
);
console.log(normalizedItems);
// [
// {
// orderId: 'ORD-001',
// customerId: 'CUST-123',
// customerName: '田中太郎',
// customerEmail: 'tanaka@example.com',
// productId: 'PROD-A001',
// productName: 'ワイヤレスマウス',
// price: 3500,
// quantity: 2,
// totalPrice: 7000
// },
// {
// orderId: 'ORD-001',
// customerId: 'CUST-123',
// customerName: '田中太郎',
// customerEmail: 'tanaka@example.com',
// productId: 'PROD-B002',
// productName: 'USB充電ケーブル',
// price: 1200,
// quantity: 3,
// totalPrice: 3600
// },
// {
// orderId: 'ORD-002',
// customerId: 'CUST-456',
// customerName: '佐藤花子',
// customerEmail: 'sato@example.com',
// productId: 'PROD-C003',
// productName: 'ノートPC',
// price: 120000,
// quantity: 1,
// totalPrice: 120000
// }
// ]
get
関数の第 3 引数にデフォルト値を指定することで、さらに安全なアクセスが可能です。
javascript// デフォルト値を指定した安全なアクセス
const safeNormalizedItems = flatMap(
apiResponse.data.orders,
(order) => {
return order.items.map((item) => ({
orderId: order.orderId,
customerId: get(order, 'customer.id', 'UNKNOWN'),
customerName: get(
order,
'customer.profile.name',
'名前なし'
),
customerEmail: get(
order,
'customer.profile.contact.email',
'no-email@example.com'
),
productId: item.productId,
productName: item.name,
price: item.price,
quantity: item.quantity,
totalPrice: item.price * item.quantity,
}));
}
);
Step 2: uniqBy で重複データを除去
正規化したデータから、特定のキーで重複を除去するには uniqBy
を使います。
javascriptimport { uniqBy } from 'lodash';
// 顧客情報の重複を除去
const uniqueCustomers = uniqBy(
normalizedItems.map((item) => ({
id: item.customerId,
name: item.customerName,
email: item.customerEmail,
})),
'id'
);
console.log(uniqueCustomers);
// [
// { id: 'CUST-123', name: '田中太郎', email: 'tanaka@example.com' },
// { id: 'CUST-456', name: '佐藤花子', email: 'sato@example.com' }
// ]
// 商品情報の重複を除去
const uniqueProducts = uniqBy(
normalizedItems.map((item) => ({
id: item.productId,
name: item.productName,
price: item.price,
})),
'id'
);
console.log(uniqueProducts);
// [
// { id: 'PROD-A001', name: 'ワイヤレスマウス', price: 3500 },
// { id: 'PROD-B002', name: 'USB充電ケーブル', price: 1200 },
// { id: 'PROD-C003', name: 'ノートPC', price: 120000 }
// ]
複数のフィールドの組み合わせで重複判定を行う場合は、関数を指定します。
javascript// 注文IDと商品IDの組み合わせで重複判定
const uniqueOrderItems = uniqBy(
normalizedItems,
(item) => `${item.orderId}_${item.productId}`
);
Step 3: keyBy と mapValues でキー変換
配列データをオブジェクトに変換し、ID によるアクセスを高速化するには keyBy
を使います。
javascriptimport { keyBy, mapValues } from 'lodash';
// 顧客データをIDでキー化
const customersById = keyBy(uniqueCustomers, 'id');
console.log(customersById);
// {
// 'CUST-123': { id: 'CUST-123', name: '田中太郎', email: 'tanaka@example.com' },
// 'CUST-456': { id: 'CUST-456', name: '佐藤花子', email: 'sato@example.com' }
// }
// 商品データをIDでキー化
const productsById = keyBy(uniqueProducts, 'id');
mapValues
を使って、キー化したオブジェクトの各値を変換できます。
javascript// 顧客データに追加情報を付与
const enrichedCustomers = mapValues(
customersById,
(customer) => ({
...customer,
displayName: `${customer.name} (${customer.id})`,
domain: customer.email.split('@')[1],
})
);
console.log(enrichedCustomers);
// {
// 'CUST-123': {
// id: 'CUST-123',
// name: '田中太郎',
// email: 'tanaka@example.com',
// displayName: '田中太郎 (CUST-123)',
// domain: 'example.com'
// },
// ...
// }
Step 4: groupBy で集計軸ごとにグルーピング
データを特定の軸でグルーピングするには groupBy
を使います。
javascriptimport { groupBy } from 'lodash';
// 顧客別にグルーピング
const itemsByCustomer = groupBy(
normalizedItems,
'customerId'
);
console.log(itemsByCustomer);
// {
// 'CUST-123': [
// { orderId: 'ORD-001', customerId: 'CUST-123', productId: 'PROD-A001', ... },
// { orderId: 'ORD-001', customerId: 'CUST-123', productId: 'PROD-B002', ... }
// ],
// 'CUST-456': [
// { orderId: 'ORD-002', customerId: 'CUST-456', productId: 'PROD-C003', ... }
// ]
// }
// 商品別にグルーピング
const itemsByProduct = groupBy(
normalizedItems,
'productId'
);
// 注文別にグルーピング
const itemsByOrder = groupBy(normalizedItems, 'orderId');
関数を使った動的なグルーピングも可能です。
javascript// 価格帯別にグルーピング
const itemsByPriceRange = groupBy(
normalizedItems,
(item) => {
if (item.price < 5000) return '低価格';
if (item.price < 50000) return '中価格';
return '高価格';
}
);
console.log(itemsByPriceRange);
// {
// 低価格: [...],
// 中価格: [...],
// 高価格: [...]
// }
Step 5: sumBy・meanBy・countBy で集計値を計算
グルーピングしたデータから集計値を計算するには、sumBy
、meanBy
、countBy
などを使います。
javascriptimport { sumBy, meanBy, countBy } from 'lodash';
// 顧客別の購入金額を集計
const customerStats = mapValues(
itemsByCustomer,
(items) => ({
// 合計金額
totalAmount: sumBy(items, 'totalPrice'),
// 平均単価
averagePrice: Math.round(meanBy(items, 'price')),
// 商品数
itemCount: items.length,
// 合計購入個数
totalQuantity: sumBy(items, 'quantity'),
})
);
console.log(customerStats);
// {
// 'CUST-123': {
// totalAmount: 10600,
// averagePrice: 2350,
// itemCount: 2,
// totalQuantity: 5
// },
// 'CUST-456': {
// totalAmount: 120000,
// averagePrice: 120000,
// itemCount: 1,
// totalQuantity: 1
// }
// }
複数の集計軸を組み合わせた高度な集計も可能です。
javascript// 商品別の販売統計
const productStats = mapValues(itemsByProduct, (items) => ({
// 販売合計金額
totalRevenue: sumBy(items, 'totalPrice'),
// 販売個数
totalSold: sumBy(items, 'quantity'),
// 購入顧客数
uniqueCustomers: uniqBy(items, 'customerId').length,
// 平均購入個数
averageQuantity: meanBy(items, 'quantity'),
// 価格
price: items[0].price,
}));
console.log(productStats);
// {
// 'PROD-A001': {
// totalRevenue: 7000,
// totalSold: 2,
// uniqueCustomers: 1,
// averageQuantity: 2,
// price: 3500
// },
// ...
// }
countBy
を使うと、特定の条件でのカウント集計が簡単にできます。
javascript// 価格帯別の商品数をカウント
const countByPriceRange = countBy(
normalizedItems,
(item) => {
if (item.price < 5000) return '低価格';
if (item.price < 50000) return '中価格';
return '高価格';
}
);
console.log(countByPriceRange);
// { 低価格: 2, 高価格: 1 }
Step 6: sortBy・orderBy で並べ替え
集計結果を並べ替えるには sortBy
や orderBy
を使います。
javascriptimport { sortBy, orderBy } from 'lodash';
// 商品統計を売上順にソート
const sortedProductStats = sortBy(
Object.entries(productStats).map(([id, stats]) => ({
productId: id,
...stats,
})),
'totalRevenue'
).reverse(); // 降順にするために反転
console.log(sortedProductStats);
// [
// { productId: 'PROD-C003', totalRevenue: 120000, ... },
// { productId: 'PROD-A001', totalRevenue: 7000, ... },
// { productId: 'PROD-B002', totalRevenue: 3600, ... }
// ]
orderBy
を使うと、複数キーでのソートや昇順・降順の指定が簡単にできます。
javascript// 売上降順、次に商品ID昇順でソート
const orderedProductStats = orderBy(
Object.entries(productStats).map(([id, stats]) => ({
productId: id,
...stats,
})),
['totalRevenue', 'productId'], // ソートキー
['desc', 'asc'] // ソート方向
);
console.log(orderedProductStats);
カスタム関数でのソートも可能です。
javascript// ROI(投資対効果)でソート
const sortedByROI = orderBy(
Object.entries(productStats).map(([id, stats]) => ({
productId: id,
...stats,
roi: stats.totalRevenue / stats.price, // カスタム計算
})),
[(item) => item.roi], // カスタム関数
['desc']
);
Step 7: map・pick・omit で UI 表示用に整形
最後に、UI コンポーネントが必要とする形式にデータを整形します。
javascriptimport { map, pick, omit } from 'lodash';
// Chart.js用のデータ形式に整形
const chartData = {
labels: map(orderedProductStats, 'productId'),
datasets: [
{
label: '売上',
data: map(orderedProductStats, 'totalRevenue'),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1,
},
],
};
console.log(chartData);
// {
// labels: ['PROD-C003', 'PROD-A001', 'PROD-B002'],
// datasets: [
// {
// label: '売上',
// data: [120000, 7000, 3600],
// ...
// }
// ]
// }
pick
を使って、必要なフィールドのみを抽出できます。
javascript// テーブル表示用に必要なフィールドのみ抽出
const tableData = orderedProductStats.map((item) =>
pick(item, [
'productId',
'totalRevenue',
'totalSold',
'uniqueCustomers',
])
);
console.log(tableData);
// [
// { productId: 'PROD-C003', totalRevenue: 120000, totalSold: 1, uniqueCustomers: 1 },
// { productId: 'PROD-A001', totalRevenue: 7000, totalSold: 2, uniqueCustomers: 1 },
// ...
// ]
omit
を使って、特定のフィールドを除外することもできます。
javascript// 内部IDを除外してAPI用に整形
const apiResponse = orderedProductStats.map((item) =>
omit(item, ['internalId', 'privateData'])
);
セレクトボックス用のオプション形式への変換も簡単です。
javascript// セレクトボックス用のオプションに整形
const selectOptions = uniqueProducts.map((product) => ({
value: product.id,
label: `${
product.name
} - ¥${product.price.toLocaleString()}`,
}));
console.log(selectOptions);
// [
// { value: 'PROD-A001', label: 'ワイヤレスマウス - ¥3,500' },
// { value: 'PROD-B002', label: 'USB充電ケーブル - ¥1,200' },
// { value: 'PROD-C003', label: 'ノートPC - ¥120,000' }
// ]
具体例
ここからは、実際のユースケースに基づいた具体的な実装例を見ていきましょう。
EC サイトの売上分析ダッシュボード
EC サイトの管理画面で、売上データを多角的に分析するダッシュボードを実装します。
まず、API から取得する生のデータを準備します。
javascript// APIから取得した生の売上データ
const rawSalesData = {
status: 'success',
data: {
transactions: [
{
transactionId: 'TXN-2024-001',
timestamp: '2024-01-15T10:30:00Z',
customer: {
id: 'CUST-001',
profile: {
name: '田中太郎',
tier: 'gold',
region: '関東',
},
},
cart: {
items: [
{
product: {
id: 'PROD-A001',
name: 'ワイヤレスマウス',
category: {
main: '電子機器',
sub: '周辺機器',
},
price: 3500,
},
quantity: 2,
discount: { type: 'percentage', value: 10 },
},
{
product: {
id: 'PROD-B002',
name: 'USB-Cケーブル',
category: {
main: '電子機器',
sub: 'ケーブル',
},
price: 1200,
},
quantity: 3,
discount: { type: 'fixed', value: 200 },
},
],
subtotal: 9600,
tax: 960,
total: 10560,
},
},
{
transactionId: 'TXN-2024-002',
timestamp: '2024-01-15T14:20:00Z',
customer: {
id: 'CUST-002',
profile: {
name: '佐藤花子',
tier: 'silver',
region: '関西',
},
},
cart: {
items: [
{
product: {
id: 'PROD-C003',
name: 'ノートPC',
category: {
main: '電子機器',
sub: 'コンピュータ',
},
price: 120000,
},
quantity: 1,
discount: { type: 'percentage', value: 5 },
},
],
subtotal: 114000,
tax: 11400,
total: 125400,
},
},
{
transactionId: 'TXN-2024-003',
timestamp: '2024-01-16T09:15:00Z',
customer: {
id: 'CUST-001',
profile: {
name: '田中太郎',
tier: 'gold',
region: '関東',
},
},
cart: {
items: [
{
product: {
id: 'PROD-D004',
name: 'ワイヤレスキーボード',
category: {
main: '電子機器',
sub: '周辺機器',
},
price: 8500,
},
quantity: 1,
discount: { type: 'percentage', value: 15 },
},
],
subtotal: 7225,
tax: 722,
total: 7947,
},
},
],
},
};
この生データに対して、7 ステップの処理を適用していきます。
Step 1-3: 正規化・重複排除・キー変換
javascriptimport {
flatMap,
get,
uniqBy,
keyBy,
groupBy,
sumBy,
meanBy,
orderBy,
map,
} from 'lodash';
// Step 1: フラットに正規化
const normalizedTransactions = flatMap(
rawSalesData.data.transactions,
(txn) => {
return txn.cart.items.map((item) => ({
// 取引情報
transactionId: txn.transactionId,
date: txn.timestamp.split('T')[0],
datetime: txn.timestamp,
// 顧客情報
customerId: get(txn, 'customer.id'),
customerName: get(txn, 'customer.profile.name'),
customerTier: get(txn, 'customer.profile.tier'),
region: get(txn, 'customer.profile.region'),
// 商品情報
productId: get(item, 'product.id'),
productName: get(item, 'product.name'),
category: get(item, 'product.category.main'),
subCategory: get(item, 'product.category.sub'),
unitPrice: get(item, 'product.price'),
quantity: item.quantity,
// 割引情報
discountType: get(item, 'discount.type'),
discountValue: get(item, 'discount.value'),
// 計算フィールド
subtotal: item.product.price * item.quantity,
discountAmount:
item.discount.type === 'percentage'
? Math.round(
item.product.price *
item.quantity *
(item.discount.value / 100)
)
: item.discount.value,
}));
}
);
// 割引後の金額を計算
const enrichedTransactions = normalizedTransactions.map(
(item) => ({
...item,
finalAmount: item.subtotal - item.discountAmount,
})
);
// Step 2: 顧客情報の重複を除去
const uniqueCustomers = uniqBy(
enrichedTransactions.map((item) => ({
id: item.customerId,
name: item.customerName,
tier: item.customerTier,
region: item.region,
})),
'id'
);
// Step 3: 顧客をIDでキー化
const customersById = keyBy(uniqueCustomers, 'id');
Step 4-5: グルーピングと集計
javascript// Step 4: 各種軸でグルーピング
const byCustomer = groupBy(
enrichedTransactions,
'customerId'
);
const byProduct = groupBy(
enrichedTransactions,
'productId'
);
const byCategory = groupBy(
enrichedTransactions,
'category'
);
const byRegion = groupBy(enrichedTransactions, 'region');
const byDate = groupBy(enrichedTransactions, 'date');
// Step 5: 顧客別売上を集計
const customerSalesStats = map(
Object.entries(byCustomer),
([customerId, items]) => ({
customerId,
customerName: customersById[customerId].name,
tier: customersById[customerId].tier,
region: customersById[customerId].region,
// 購入回数
transactionCount: uniqBy(items, 'transactionId').length,
// 購入商品数
totalItems: sumBy(items, 'quantity'),
// 合計金額
totalAmount: sumBy(items, 'finalAmount'),
// 平均単価
averagePrice: Math.round(meanBy(items, 'unitPrice')),
// 平均割引率
averageDiscountRate:
Math.round(
meanBy(items, (item) =>
item.discountType === 'percentage'
? item.discountValue
: (item.discountAmount / item.subtotal) * 100
)
) || 0,
})
);
// 商品別売上を集計
const productSalesStats = map(
Object.entries(byProduct),
([productId, items]) => ({
productId,
productName: items[0].productName,
category: items[0].category,
subCategory: items[0].subCategory,
unitPrice: items[0].unitPrice,
// 販売個数
totalSold: sumBy(items, 'quantity'),
// 売上金額
revenue: sumBy(items, 'finalAmount'),
// 購入顧客数
uniqueCustomers: uniqBy(items, 'customerId').length,
// 取引回数
transactionCount: uniqBy(items, 'transactionId').length,
})
);
// カテゴリ別売上を集計
const categorySalesStats = map(
Object.entries(byCategory),
([category, items]) => ({
category,
// 商品種類数
productCount: uniqBy(items, 'productId').length,
// 販売個数
totalSold: sumBy(items, 'quantity'),
// 売上金額
revenue: sumBy(items, 'finalAmount'),
// 顧客数
uniqueCustomers: uniqBy(items, 'customerId').length,
})
);
// 地域別売上を集計
const regionSalesStats = map(
Object.entries(byRegion),
([region, items]) => ({
region,
// 取引回数
transactionCount: uniqBy(items, 'transactionId').length,
// 顧客数
customerCount: uniqBy(items, 'customerId').length,
// 売上金額
revenue: sumBy(items, 'finalAmount'),
// 平均単価
averageOrderValue: Math.round(
sumBy(items, 'finalAmount') /
uniqBy(items, 'transactionId').length
),
})
);
// 日別売上を集計
const dailySalesStats = map(
Object.entries(byDate),
([date, items]) => ({
date,
// 取引回数
transactionCount: uniqBy(items, 'transactionId').length,
// 商品販売個数
itemsSold: sumBy(items, 'quantity'),
// 売上金額
revenue: sumBy(items, 'finalAmount'),
})
);
Step 6-7: ソートと整形
javascript// Step 6: 各統計をソート
const topCustomers = orderBy(
customerSalesStats,
['totalAmount'],
['desc']
);
const topProducts = orderBy(
productSalesStats,
['revenue'],
['desc']
);
const topCategories = orderBy(
categorySalesStats,
['revenue'],
['desc']
);
// Step 7: UI表示用に整形
// ダッシュボード用サマリー
const dashboardSummary = {
// 全体サマリー
overview: {
totalRevenue: sumBy(
enrichedTransactions,
'finalAmount'
),
totalTransactions: uniqBy(
enrichedTransactions,
'transactionId'
).length,
totalCustomers: uniqueCustomers.length,
totalItemsSold: sumBy(enrichedTransactions, 'quantity'),
averageOrderValue: Math.round(
sumBy(enrichedTransactions, 'finalAmount') /
uniqBy(enrichedTransactions, 'transactionId').length
),
},
// 売上チャート用データ(Chart.js形式)
salesChart: {
labels: map(dailySalesStats, 'date'),
datasets: [
{
label: '売上',
data: map(dailySalesStats, 'revenue'),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 2,
},
],
},
// カテゴリ別円グラフ用データ
categoryPieChart: {
labels: map(topCategories, 'category'),
datasets: [
{
data: map(topCategories, 'revenue'),
backgroundColor: [
'rgba(255, 99, 132, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(255, 206, 86, 0.6)',
],
},
],
},
// トップ顧客テーブル用データ
topCustomersTable: topCustomers.slice(0, 10).map((c) => ({
顧客名: c.customerName,
会員ランク: c.tier,
地域: c.region,
購入回数: c.transactionCount,
合計金額: `¥${c.totalAmount.toLocaleString()}`,
})),
// トップ商品テーブル用データ
topProductsTable: topProducts.slice(0, 10).map((p) => ({
商品名: p.productName,
カテゴリ: p.category,
単価: `¥${p.unitPrice.toLocaleString()}`,
販売個数: p.totalSold,
売上: `¥${p.revenue.toLocaleString()}`,
})),
// 地域別比較データ
regionComparison: regionSalesStats.map((r) => ({
地域: r.region,
売上: r.revenue,
顧客数: r.customerCount,
平均単価: r.averageOrderValue,
})),
};
console.log(dashboardSummary);
このように、7 ステップを順番に適用することで、複雑な生データから UI 表示に必要な形式まで、体系的にデータ変換を実現できます。
ユーザー行動ログの分析レポート
次に、Web アプリケーションのユーザー行動ログを分析するケースを見てみましょう。
javascript// 生のログデータ
const rawLogs = {
logs: [
{
sessionId: 'SES-001',
user: {
id: 'USER-123',
info: { name: '田中太郎', plan: 'premium' },
},
events: [
{
type: 'page_view',
page: '/dashboard',
timestamp: '2024-01-15T10:00:00Z',
duration: 45,
},
{
type: 'button_click',
target: 'create_project',
timestamp: '2024-01-15T10:00:45Z',
},
{
type: 'page_view',
page: '/project/new',
timestamp: '2024-01-15T10:00:50Z',
duration: 120,
},
],
},
{
sessionId: 'SES-002',
user: {
id: 'USER-456',
info: { name: '佐藤花子', plan: 'free' },
},
events: [
{
type: 'page_view',
page: '/pricing',
timestamp: '2024-01-15T11:00:00Z',
duration: 30,
},
{
type: 'button_click',
target: 'upgrade_plan',
timestamp: '2024-01-15T11:00:30Z',
},
],
},
{
sessionId: 'SES-003',
user: {
id: 'USER-123',
info: { name: '田中太郎', plan: 'premium' },
},
events: [
{
type: 'page_view',
page: '/dashboard',
timestamp: '2024-01-16T09:00:00Z',
duration: 60,
},
{
type: 'button_click',
target: 'export_data',
timestamp: '2024-01-16T09:01:00Z',
},
],
},
],
};
7 ステップで分析用データに変換します。
javascript// Step 1: 正規化
const normalizedEvents = flatMap(
rawLogs.logs,
(session) => {
return session.events.map((event) => ({
sessionId: session.sessionId,
userId: get(session, 'user.id'),
userName: get(session, 'user.info.name'),
userPlan: get(session, 'user.info.plan'),
eventType: event.type,
page: event.page,
target: event.target,
timestamp: event.timestamp,
date: event.timestamp.split('T')[0],
hour: parseInt(
event.timestamp.split('T')[1].split(':')[0]
),
duration: event.duration || 0,
}));
}
);
// Step 2-3: ユーザー情報の抽出とキー化
const uniqueUsers = uniqBy(
normalizedEvents.map((e) => ({
id: e.userId,
name: e.userName,
plan: e.userPlan,
})),
'id'
);
const usersById = keyBy(uniqueUsers, 'id');
// Step 4: グルーピング
const byUser = groupBy(normalizedEvents, 'userId');
const byEventType = groupBy(normalizedEvents, 'eventType');
const byPage = groupBy(normalizedEvents, 'page');
const byPlan = groupBy(normalizedEvents, 'userPlan');
const byDate = groupBy(normalizedEvents, 'date');
const byHour = groupBy(normalizedEvents, 'hour');
// Step 5: ユーザー別行動分析
const userBehaviorStats = map(
Object.entries(byUser),
([userId, events]) => ({
userId,
userName: usersById[userId].name,
plan: usersById[userId].plan,
// セッション数
sessionCount: uniqBy(events, 'sessionId').length,
// 総イベント数
totalEvents: events.length,
// ページビュー数
pageViews: events.filter(
(e) => e.eventType === 'page_view'
).length,
// クリック数
clicks: events.filter(
(e) => e.eventType === 'button_click'
).length,
// 平均滞在時間
averageDuration: Math.round(
meanBy(
events.filter((e) => e.duration > 0),
'duration'
) || 0
),
// アクセス日数
activeDays: uniqBy(events, 'date').length,
})
);
// イベントタイプ別統計
const eventTypeStats = map(
Object.entries(byEventType),
([type, events]) => ({
eventType: type,
count: events.length,
uniqueUsers: uniqBy(events, 'userId').length,
uniqueSessions: uniqBy(events, 'sessionId').length,
})
);
// ページ別統計
const pageStats = map(
Object.entries(byPage),
([page, events]) => {
const pageViewEvents = events.filter(
(e) => e.eventType === 'page_view'
);
return {
page,
views: pageViewEvents.length,
uniqueVisitors: uniqBy(pageViewEvents, 'userId')
.length,
averageDuration: Math.round(
meanBy(pageViewEvents, 'duration') || 0
),
totalDuration: sumBy(pageViewEvents, 'duration'),
};
}
);
// プラン別統計
const planStats = map(
Object.entries(byPlan),
([plan, events]) => ({
plan,
users: uniqBy(events, 'userId').length,
sessions: uniqBy(events, 'sessionId').length,
averageEventsPerUser: Math.round(
events.length / uniqBy(events, 'userId').length
),
})
);
// 時間帯別アクセス統計
const hourlyStats = map(
Object.entries(byHour),
([hour, events]) => ({
hour: parseInt(hour),
events: events.length,
uniqueUsers: uniqBy(events, 'userId').length,
})
);
// Step 6: ソート
const topUsers = orderBy(
userBehaviorStats,
['totalEvents'],
['desc']
);
const topPages = orderBy(pageStats, ['views'], ['desc']);
const sortedHourlyStats = orderBy(
hourlyStats,
['hour'],
['asc']
);
// Step 7: レポート用に整形
const behaviorAnalysisReport = {
// サマリー
summary: {
totalUsers: uniqueUsers.length,
totalSessions: uniqBy(normalizedEvents, 'sessionId')
.length,
totalEvents: normalizedEvents.length,
averageEventsPerSession: Math.round(
normalizedEvents.length /
uniqBy(normalizedEvents, 'sessionId').length
),
},
// 時間帯別アクセスチャート
hourlyAccessChart: {
labels: map(sortedHourlyStats, (s) => `${s.hour}時`),
datasets: [
{
label: 'イベント数',
data: map(sortedHourlyStats, 'events'),
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
},
{
label: 'アクティブユーザー',
data: map(sortedHourlyStats, 'uniqueUsers'),
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
},
],
},
// プラン別比較テーブル
planComparisonTable: planStats.map((p) => ({
プラン: p.plan,
ユーザー数: p.users,
セッション数: p.sessions,
平均イベント数: p.averageEventsPerUser,
})),
// トップページテーブル
topPagesTable: topPages.map((p) => ({
ページ: p.page,
閲覧数: p.views,
ユニーク訪問者: p.uniqueVisitors,
平均滞在時間: `${p.averageDuration}秒`,
})),
// アクティブユーザーランキング
activeUsersRanking: topUsers
.slice(0, 10)
.map((u, index) => ({
順位: index + 1,
ユーザー名: u.userName,
プラン: u.plan,
イベント数: u.totalEvents,
セッション数: u.sessionCount,
アクセス日数: u.activeDays,
})),
};
console.log(behaviorAnalysisReport);
マルチレベルカテゴリの階層的集計
最後に、複雑な階層構造を持つカテゴリデータを集計する例を見てみましょう。
javascript// 階層的なカテゴリを持つ商品データ
const rawProductData = {
categories: [
{
id: 'CAT-1',
name: '電子機器',
subcategories: [
{
id: 'SUBCAT-1-1',
name: 'コンピュータ',
products: [
{
id: 'PROD-001',
name: 'ノートPC',
price: 120000,
stock: 15,
sales: [
{
date: '2024-01-15',
quantity: 2,
revenue: 240000,
},
{
date: '2024-01-16',
quantity: 1,
revenue: 120000,
},
],
},
{
id: 'PROD-002',
name: 'デスクトップPC',
price: 150000,
stock: 8,
sales: [
{
date: '2024-01-15',
quantity: 1,
revenue: 150000,
},
],
},
],
},
{
id: 'SUBCAT-1-2',
name: '周辺機器',
products: [
{
id: 'PROD-003',
name: 'マウス',
price: 3500,
stock: 50,
sales: [
{
date: '2024-01-15',
quantity: 5,
revenue: 17500,
},
{
date: '2024-01-16',
quantity: 3,
revenue: 10500,
},
],
},
],
},
],
},
{
id: 'CAT-2',
name: '家電',
subcategories: [
{
id: 'SUBCAT-2-1',
name: '調理家電',
products: [
{
id: 'PROD-004',
name: '電子レンジ',
price: 25000,
stock: 20,
sales: [
{
date: '2024-01-15',
quantity: 2,
revenue: 50000,
},
],
},
],
},
],
},
],
};
7 ステップで階層的に集計します。
javascript// Step 1: 完全に正規化
const normalizedSales = flatMap(
rawProductData.categories,
(category) => {
return flatMap(
category.subcategories,
(subcategory) => {
return flatMap(subcategory.products, (product) => {
return product.sales.map((sale) => ({
// カテゴリ情報
categoryId: category.id,
categoryName: category.name,
subcategoryId: subcategory.id,
subcategoryName: subcategory.name,
// 商品情報
productId: product.id,
productName: product.name,
unitPrice: product.price,
stock: product.stock,
// 売上情報
date: sale.date,
quantity: sale.quantity,
revenue: sale.revenue,
}));
});
}
);
}
);
// Step 2-3: 各エンティティの抽出とキー化
const uniqueCategories = uniqBy(
normalizedSales.map((s) => ({
id: s.categoryId,
name: s.categoryName,
})),
'id'
);
const uniqueSubcategories = uniqBy(
normalizedSales.map((s) => ({
id: s.subcategoryId,
name: s.subcategoryName,
categoryId: s.categoryId,
})),
'id'
);
const uniqueProducts = uniqBy(
normalizedSales.map((s) => ({
id: s.productId,
name: s.productName,
price: s.unitPrice,
subcategoryId: s.subcategoryId,
})),
'id'
);
// Step 4: 階層的グルーピング
const byCategory = groupBy(normalizedSales, 'categoryId');
const bySubcategory = groupBy(
normalizedSales,
'subcategoryId'
);
const byProduct = groupBy(normalizedSales, 'productId');
const byDate = groupBy(normalizedSales, 'date');
// Step 5: 各レベルで集計
// 商品レベルの集計
const productLevelStats = map(
Object.entries(byProduct),
([productId, sales]) => {
const product = uniqueProducts.find(
(p) => p.id === productId
);
return {
productId,
productName: product.name,
subcategoryId: product.subcategoryId,
unitPrice: product.price,
totalSold: sumBy(sales, 'quantity'),
totalRevenue: sumBy(sales, 'revenue'),
salesCount: sales.length,
};
}
);
// サブカテゴリレベルの集計
const subcategoryLevelStats = map(
Object.entries(bySubcategory),
([subcategoryId, sales]) => {
const subcategory = uniqueSubcategories.find(
(s) => s.id === subcategoryId
);
const productsInSubcategory = productLevelStats.filter(
(p) => p.subcategoryId === subcategoryId
);
return {
subcategoryId,
subcategoryName: subcategory.name,
categoryId: subcategory.categoryId,
productCount: productsInSubcategory.length,
totalRevenue: sumBy(
productsInSubcategory,
'totalRevenue'
),
totalItemsSold: sumBy(
productsInSubcategory,
'totalSold'
),
averageProductPrice: Math.round(
meanBy(productsInSubcategory, 'unitPrice')
),
};
}
);
// カテゴリレベルの集計
const categoryLevelStats = map(
Object.entries(byCategory),
([categoryId, sales]) => {
const category = uniqueCategories.find(
(c) => c.id === categoryId
);
const subcategoriesInCategory =
subcategoryLevelStats.filter(
(s) => s.categoryId === categoryId
);
return {
categoryId,
categoryName: category.name,
subcategoryCount: subcategoriesInCategory.length,
totalRevenue: sumBy(
subcategoriesInCategory,
'totalRevenue'
),
totalItemsSold: sumBy(
subcategoriesInCategory,
'totalItemsSold'
),
productCount: sumBy(
subcategoriesInCategory,
'productCount'
),
};
}
);
// Step 6: ソート
const topCategories = orderBy(
categoryLevelStats,
['totalRevenue'],
['desc']
);
const topSubcategories = orderBy(
subcategoryLevelStats,
['totalRevenue'],
['desc']
);
const topProducts = orderBy(
productLevelStats,
['totalRevenue'],
['desc']
);
// Step 7: 階層的レポートに整形
const hierarchicalSalesReport = {
// 全体サマリー
overall: {
totalRevenue: sumBy(categoryLevelStats, 'totalRevenue'),
totalCategories: uniqueCategories.length,
totalSubcategories: uniqueSubcategories.length,
totalProducts: uniqueProducts.length,
totalItemsSold: sumBy(
categoryLevelStats,
'totalItemsSold'
),
},
// カテゴリ別階層データ
hierarchicalData: topCategories.map((category) => {
const subcategories = topSubcategories
.filter((s) => s.categoryId === category.categoryId)
.map((subcategory) => {
const products = topProducts
.filter(
(p) =>
p.subcategoryId === subcategory.subcategoryId
)
.map((product) => ({
商品名: product.productName,
単価: `¥${product.unitPrice.toLocaleString()}`,
販売数: product.totalSold,
売上: `¥${product.totalRevenue.toLocaleString()}`,
}));
return {
サブカテゴリ名: subcategory.subcategoryName,
商品数: subcategory.productCount,
売上: `¥${subcategory.totalRevenue.toLocaleString()}`,
商品詳細: products,
};
});
return {
カテゴリ名: category.categoryName,
サブカテゴリ数: category.subcategoryCount,
商品数: category.productCount,
売上: `¥${category.totalRevenue.toLocaleString()}`,
サブカテゴリ詳細: subcategories,
};
}),
// カテゴリ別売上チャート
categorySalesChart: {
labels: map(topCategories, 'categoryName'),
datasets: [
{
label: '売上',
data: map(topCategories, 'totalRevenue'),
backgroundColor: [
'rgba(255, 99, 132, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(255, 206, 86, 0.6)',
],
},
],
},
// トップ商品ランキング
topProductsRanking: topProducts
.slice(0, 10)
.map((p, index) => ({
順位: index + 1,
商品名: p.productName,
単価: `¥${p.unitPrice.toLocaleString()}`,
販売数: p.totalSold,
売上: `¥${p.totalRevenue.toLocaleString()}`,
})),
};
console.log(
JSON.stringify(hierarchicalSalesReport, null, 2)
);
まとめ
Lodash の各種関数を組み合わせることで、巨大で複雑な JSON データを「正規化 → 集計 → 整形」する処理を、体系的かつ効率的に実装できます。
本記事で解説した 7 ステップをまとめますと:
7 ステップの概要
# | ステップ | 使用する主な関数 | 目的 |
---|---|---|---|
1 | 正規化 | flatMap , get | ネストしたデータをフラット化 |
2 | 重複排除 | uniqBy | 重複データを除去 |
3 | キー変換 | keyBy , mapValues | 配列をオブジェクトに変換 |
4 | グルーピング | groupBy | 集計軸ごとにデータを分類 |
5 | 集計 | sumBy , meanBy , countBy | 各種統計値を計算 |
6 | ソート | sortBy , orderBy | 結果を並べ替え |
7 | 整形 | map , pick , omit | UI 表示用の形式に変換 |
実装における重要なポイント
get
関数により、null や undefined の安全なチェックが可能flatMap
により、ネストした配列を 1 階層フラット化keyBy
により、配列からオブジェクトへの変換が簡単groupBy
により、複数の集計軸での分類が容易- 各ステップは独立しており、必要に応じて組み合わせ可能
適用場面
- EC サイトの売上分析ダッシュボード
- ユーザー行動ログの集計レポート
- マルチレベルカテゴリの階層的集計
- API レスポンスの正規化と UI 表示用データへの変換
パフォーマンスの考慮点
- 大量データ(10 万件以上)では、処理を分割してメモリ負荷を軽減
- 頻繁に実行される処理は、React の
useMemo
や Vue のcomputed
でメモ化 - 不要なデータのコピーを避け、必要なフィールドのみを抽出
この 7 ステップのパターンをマスターすれば、どんなに複雑な JSON データでも、読みやすく保守しやすいコードで効率的に処理できるようになります。
特に、現代の SPA(Single Page Application)開発では、API から取得したデータを適切に変換して UI に表示する処理が頻繁に発生しますので、この手法は非常に実用的でしょう。
ぜひ、実際のプロジェクトでこの 7 ステップのパターンを試してみてください。データ処理が驚くほどシンプルになり、開発効率が大幅に向上するはずです。
関連リンク
- article
Lodash で巨大 JSON を“正規化 → 集計 → 整形”する 7 ステップ実装
- article
Lodash クイックレシピ :配列・オブジェクト変換の“定番ひな形”集
- article
Lodash を部分インポートで導入する最短ルート:ESM/TS/バンドラ別の設定集
- article
Lodash の全体像を 1 枚絵で把握する:配列・オブジェクト・関数操作の設計マップ
- article
Lodash の throttle・debounce でパフォーマンス最適化
- article
Lodash の chain で複雑なデータ処理をシンプルに
- article
NotebookLM とは?Google 製 AI ノートの仕組みとできることを 3 分で解説
- article
Mermaid 図面の命名規約:ノード ID・エッジ記法・クラス名の統一ガイド
- article
Emotion 初期設定完全ガイド:Babel/SWC/型定義/型拡張のベストプラクティス
- article
Electron セキュリティ設定チートシート:webPreferences/CSP/許可リスト早見表
- article
MCP サーバー とは?Model Context Protocol の基礎・仕組み・活用メリットを徹底解説
- article
Yarn とは?npm・pnpm と何が違うのかを 3 分で理解【決定版】
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来