メインコンテンツまでスキップ

第 3 回: CSV インポートで始めるグラフ構築

実際のプロジェクトデータセットで学ぶ効率的なデータモデリング

CSV Import Graph Building

🎯 この章で学ぶこと

  • 実際のプロジェクトデータを使った CSV インポート手法
  • Docker 環境での効率的なファイル転送方法
  • LOAD CSV 句の詳細仕様と実践的なトラブルシューティング
  • データ型変換とパフォーマンス最適化の実践テクニック

📖 実体験:CSV インポートで直面した現実

不正検知システムのプロジェクトで、クライアントから提供されたのは 3 つの CSV ファイルでした:

  1. users.csv - 約 50 万件のユーザーデータ
  2. transactions.csv - 約 200 万件の取引データ
  3. accounts.csv - 約 80 万件の口座データ

当初、私は単純に LOAD CSV で全データを一度に投入しようとしました。しかし、以下の問題に直面:

  1. メモリ不足: 大きなトランザクションでメモリが枯渇
  2. データ型エラー: 文字列として解釈された数値データ
  3. 参照整合性の問題: 存在しないユーザー ID を参照する取引データ

この経験から、**「段階的なデータ投入と検証の重要性」**を学びました。

📁 サンプルデータの準備

まず、実際のプロジェクトで使用したデータ構造を簡略化したサンプルデータを準備します。

users.csv

id,name,email,city,signup_date,status
1,田中太郎,tanaka@example.com,東京,2023-01-15,active
2,佐藤花子,sato@example.com,大阪,2023-02-20,active
3,鈴木一郎,suzuki@example.com,名古屋,2023-03-10,inactive
4,高橋美咲,takahashi@example.com,福岡,2023-04-05,active
5,伊藤健太,ito@example.com,札幌,2023-05-12,active

transactions.csv

id,from_user_id,to_user_id,amount,transaction_date,type,status
1,1,2,10000,2023-06-01T10:30:00,transfer,completed
2,2,3,5000,2023-06-02T14:15:00,transfer,completed
3,1,4,25000,2023-06-03T09:45:00,transfer,completed
4,3,5,7500,2023-06-04T16:20:00,transfer,pending
5,4,1,12000,2023-06-05T11:10:00,transfer,completed

accounts.csv

id,user_id,account_type,balance,currency,created_date
1,1,checking,150000,JPY,2023-01-16
2,1,savings,500000,JPY,2023-01-16
3,2,checking,75000,JPY,2023-02-21
4,3,checking,120000,JPY,2023-03-11
5,4,checking,200000,JPY,2023-04-06
6,5,checking,90000,JPY,2023-05-13

🚀 Docker 環境でのファイル転送

ステップ 1: CSV ファイルのコンテナへの転送

# プロジェクトディレクトリでのCSVファイル作成
mkdir csv_data
cd csv_data

# 上記のサンプルデータをファイルとして保存
# users.csv, transactions.csv, accounts.csvを作成

# Memgraphコンテナにファイルを転送
docker cp users.csv memgraph-db:/tmp/users.csv
docker cp transactions.csv memgraph-db:/tmp/transactions.csv
docker cp accounts.csv memgraph-db:/tmp/accounts.csv

ステップ 2: ファイル転送の確認

# コンテナ内でのファイル確認
docker exec -it memgraph-db ls -la /tmp/*.csv

# ファイル内容の確認
docker exec -it memgraph-db head -5 /tmp/users.csv

File Transfer Verification

📊 段階的グラフ構築戦略

実際のプロジェクトで学んだ教訓に基づく、効率的なデータ投入順序:

Phase 1: マスターデータの投入(ユーザー)

最初に依存関係のないマスターデータから投入します:

-- ユーザーノードの作成
LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {
id: ToInteger(row.id),
name: row.name,
email: row.email,
city: row.city,
signup_date: LocalDateTime(row.signup_date),
status: row.status
});

重要なポイント:

  • ToInteger(): 文字列を整数に変換
  • LocalDateTime(): 日付文字列を日時型に変換
  • データ型変換を必ず行う

Phase 2: 関連データの投入(口座)

-- 口座ノードの作成と所有関係の設定
LOAD CSV FROM "/tmp/accounts.csv" WITH HEADER AS row
MATCH (u:User {id: ToInteger(row.user_id)})
CREATE (a:Account {
id: ToInteger(row.id),
account_type: row.account_type,
balance: ToInteger(row.balance),
currency: row.currency,
created_date: LocalDate(row.created_date)
})
CREATE (u)-[:OWNS]->(a);

実践的考慮事項:

  • MATCHで既存ユーザーの存在確認
  • 外部キー制約のようなチェックが重要

Phase 3: トランザクションデータの投入

-- 取引ノードと関係性の作成
LOAD CSV FROM "/tmp/transactions.csv" WITH HEADER AS row
MATCH (from_user:User {id: ToInteger(row.from_user_id)})
MATCH (to_user:User {id: ToInteger(row.to_user_id)})
CREATE (t:Transaction {
id: ToInteger(row.id),
amount: ToInteger(row.amount),
transaction_date: LocalDateTime(row.transaction_date),
type: row.type,
status: row.status
})
CREATE (from_user)-[:SENT]->(t)
CREATE (t)-[:RECEIVED_BY]->(to_user);

Graph Construction Strategy

🔍 データ検証とトラブルシューティング

基本的なデータ検証クエリ

-- 投入データの件数確認
MATCH (n:User) RETURN count(n) AS user_count;
MATCH (n:Account) RETURN count(n) AS account_count;
MATCH (n:Transaction) RETURN count(n) AS transaction_count;

-- リレーションシップの確認
MATCH (u:User)-[:OWNS]->(a:Account) RETURN count(*) AS owns_relationships;
MATCH (u:User)-[:SENT]->(t:Transaction) RETURN count(*) AS sent_relationships;

よくある問題と解決方法

問題 1: データ型変換エラー

症状: ToInteger()でエラーが発生

原因: NULL 値や空文字列、非数値データの混入

解決方法:

-- 安全なデータ型変換
LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {
id: CASE
WHEN row.id IS NOT NULL AND row.id <> ''
THEN ToInteger(row.id)
ELSE NULL
END,
name: COALESCE(row.name, 'Unknown'),
email: row.email
});

問題 2: 参照整合性エラー

症状: MATCH 部分で該当するノードが見つからない

解決方法:

-- OPTIONAL MATCHとフィルタリングの使用
LOAD CSV FROM "/tmp/transactions.csv" WITH HEADER AS row
OPTIONAL MATCH (from_user:User {id: ToInteger(row.from_user_id)})
OPTIONAL MATCH (to_user:User {id: ToInteger(row.to_user_id)})
WHERE from_user IS NOT NULL AND to_user IS NOT NULL
CREATE (t:Transaction {
id: ToInteger(row.id),
amount: ToInteger(row.amount)
})
CREATE (from_user)-[:SENT]->(t)
CREATE (t)-[:RECEIVED_BY]->(to_user);

問題 3: 大きなファイルでのメモリ不足

実際のプロジェクトでの解決方法:

-- バッチサイズを制限した投入
LOAD CSV FROM "/tmp/large_transactions.csv" WITH HEADER AS row
WHERE ToInteger(row.id) >= 1 AND ToInteger(row.id) <= 10000
MATCH (from_user:User {id: ToInteger(row.from_user_id)})
MATCH (to_user:User {id: ToInteger(row.to_user_id)})
CREATE (t:Transaction {
id: ToInteger(row.id),
amount: ToInteger(row.amount)
})
CREATE (from_user)-[:SENT]->(t)
CREATE (t)-[:RECEIVED_BY]->(to_user);

Troubleshooting Flowchart

⚡ パフォーマンス最適化のテクニック

インデックスの活用

-- 頻繁に検索されるプロパティにインデックスを作成
CREATE INDEX ON :User(id);
CREATE INDEX ON :User(email);
CREATE INDEX ON :Transaction(transaction_date);

LOAD CSV のパフォーマンス設定

実際のプロジェクトで効果的だった設定:

-- プロファイリング付きでクエリ実行
PROFILE LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {id: ToInteger(row.id), name: row.name});

大量データ投入のベストプラクティス

1. ストレージモードの一時変更(大量データの場合):

# メモリ効率を優先するモードに変更
docker exec -it memgraph-db mgconsole
-- 分析モードに切り替え(トランザクション分離性よりもパフォーマンス優先)
STORAGE MODE IN_MEMORY_ANALYTICAL;

-- データ投入実行
-- ... LOAD CSV処理 ...

-- 本番モードに戻す
STORAGE MODE IN_MEMORY_TRANSACTIONAL;

2. バッチ処理のスクリプト化:

#!/bin/bash
# batch_import.sh - 段階的データ投入スクリプト

echo "Phase 1: Importing users..."
docker exec -i memgraph-db mgconsole << 'EOF'
LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {
id: ToInteger(row.id),
name: row.name,
email: row.email,
city: row.city
});
EOF

echo "Phase 2: Importing accounts..."
docker exec -i memgraph-db mgconsole << 'EOF'
LOAD CSV FROM "/tmp/accounts.csv" WITH HEADER AS row
MATCH (u:User {id: ToInteger(row.user_id)})
CREATE (a:Account {
id: ToInteger(row.id),
account_type: row.account_type,
balance: ToInteger(row.balance)
})
CREATE (u)-[:OWNS]->(a);
EOF

echo "Data import completed!"

📈 実際のプロジェクトでの成果測定

Performance Metrics

最適化前後の比較

項目最適化前最適化後改善率
200 万件のトランザクション投入時間45 分12 分73%短縮
メモリ使用量(ピーク時)8GB4.5GB44%削減
エラー率3.2%0.1%97%削減

最適化のポイント

  1. 段階的投入: 依存関係を考慮した順序での投入
  2. データ型変換の前処理: エラーハンドリングの充実
  3. インデックス戦略: 投入前のインデックス作成
  4. バッチサイズ調整: メモリ使用量とのバランス

🔧 便利なユーティリティクエリ

データクリーニング

-- 重複ユーザーの確認
MATCH (u:User)
WITH u.email AS email, collect(u) AS users
WHERE size(users) > 1
RETURN email, size(users) AS duplicate_count;

-- 孤立したアカウント(ユーザーと紐づかない)の確認
MATCH (a:Account)
WHERE NOT (a)<-[:OWNS]-(:User)
RETURN a;

データ統計の取得

-- 基本統計情報
MATCH (u:User)
RETURN
count(u) AS total_users,
count(DISTINCT u.city) AS unique_cities,
min(u.signup_date) AS earliest_signup,
max(u.signup_date) AS latest_signup;

-- 取引金額の統計
MATCH (t:Transaction)
RETURN
count(t) AS total_transactions,
sum(t.amount) AS total_amount,
avg(t.amount) AS average_amount,
max(t.amount) AS max_amount;

次の章へ

データの投入が完了したら、第 4 回: MAGE で解き放つグラフ分析の真価で、投入したデータを使った高度な分析手法を学びましょう。


著者ノート: この章で紹介した CSV インポート手法は、実際に 5 つの異なるプロジェクトで使用し、総計 1000 万件以上のデータ投入で検証した実践的な手法です。特に段階的投入と検証の仕組みは、データ品質の向上と開発効率の両立に大きく貢献しました。