はじめに
この実験では、Python で大規模なデータセットを格納するためのメモリ効率の良い方法を学びます。また、タプル、辞書、クラス、名前付きタプルなど、レコードを表現するさまざまな方法を発見します。
さらに、さまざまなデータ構造のメモリ使用量を比較します。これらの構造間のトレードオフを理解することは、データ分析を行う Python ユーザーにとって重要です。なぜなら、これによりコードの最適化に役立つからです。
データセットの探索
まず、これから扱うデータセットを詳しく見ていきましょう。ファイル ctabus.csv は CSV (Comma-Separated Values, カンマ区切り値) 形式のファイルです。CSV ファイルは、各行が 1 つのレコードを表し、行内の値がカンマで区切られた表形式のデータを保存する一般的な方法です。この特定のファイルには、2001 年 1 月 1 日から 2013 年 8 月 31 日までのシカゴ交通局 (CTA) のバスシステムの 1 日ごとの乗車データが格納されています。
ファイルを解凍し、zip ファイルを削除します。
cd /home/labex/project
unzip ctabus.csv.zip
rm ctabus.csv.zip
このファイルの構造を理解するために、まず中身を覗いてみましょう。Python を使用してファイルを読み込み、いくつかの行を出力します。ターミナルを開き、次の Python コードを実行します。
f = open('/home/labex/project/ctabus.csv')
print(next(f)) ## ヘッダー行を読み込む
print(next(f)) ## 最初のデータ行を読み込む
print(next(f)) ## 2 番目のデータ行を読み込む
f.close()
このコードでは、まず open 関数を使用してファイルを開き、変数 f に割り当てます。next 関数は、ファイルから次の行を読み取るために使用されます。これを 3 回使用します。最初はヘッダー行を読み取るためです。ヘッダー行には通常、データセット内の列の名前が含まれています。2 回目と 3 回目は、それぞれ最初と 2 番目のデータ行を読み取ります。最後に、close メソッドを使用してファイルを閉じ、システムリソースを解放します。
次のような出力が表示されるはずです。
route,date,daytype,rides
3,01/01/2001,U,7354
4,01/01/2001,U,9288
この出力は、ファイルに 4 つのデータ列があることを示しています。各列が何を表しているかを詳しく見てみましょう。
route: これはバスの路線名または番号です。データセットの最初の列 (Column 0) です。date: これは MM/DD/YYYY 形式の日付文字列です。これは 2 番目の列 (Column 1) です。daytype: これは曜日コードで、3 番目の列 (Column 2) です。- U = 日曜日/祝日 (Sunday/Holiday)
- A = 土曜日 (Saturday)
- W = 平日 (Weekday)
rides: この列は、乗客の総数を整数として記録します。これは 4 番目の列 (Column 3) です。
rides 列は、特定の日の特定の路線でバスに乗車した人数を示しています。たとえば、上記の出力から、2001 年 1 月 1 日に 7,354 人が 3 番のバスに乗車したことがわかります。
次に、ファイル内の行数を確認しましょう。行数を知ることで、データセットのサイズを把握できます。次の Python コードを実行します。
with open('/home/labex/project/ctabus.csv') as f:
line_count = sum(1 for line in f)
print(f"Total lines in the file: {line_count}")
このコードでは、with ステートメントを使用してファイルを開きます。with を使用する利点は、処理が完了すると自動的にファイルを閉じることです。次に、ジェネレーター式 (1 for line in f) を使用して、ファイル内の各行に対して 1 のシーケンスを作成します。sum 関数は、これらの 1 をすべて合計して、ファイル内の行の総数を求めます。最後に、結果を出力します。
これにより、約 577,564 行が出力されるはずです。これは、かなりの量のデータセットを扱っていることを意味します。この大規模なデータセットは、分析して洞察を得るための十分なデータを提供してくれます。
異なる保存方法によるメモリ使用量の測定
このステップでは、データの保存方法がメモリ使用量にどのように影響するかを見ていきます。メモリ使用量は、特に大規模なデータセットを扱う場合、プログラミングにおいて重要な要素です。Python コードが使用するメモリを測定するために、Python の tracemalloc モジュールを使用します。このモジュールは、Python によるメモリ割り当てを追跡できるため非常に便利です。これを使用することで、データ保存方法がどれだけのメモリを消費しているかを確認できます。
方法 1: ファイル全体を単一の文字列として保存する
まず、新しい Python ファイルを作成しましょう。/home/labex/project ディレクトリに移動し、memory_test1.py という名前のファイルを作成します。テキストエディタを使用してこのファイルを開きます。ファイルを開いたら、以下のコードを追加します。このコードは、ファイルの内容全体を単一の文字列として読み込み、メモリ使用量を測定します。
## memory_test1.py
import tracemalloc
def test_single_string():
## メモリの追跡を開始する
tracemalloc.start()
## ファイル全体を単一の文字列として読み込む
with open('/home/labex/project/ctabus.csv') as f:
data = f.read()
## メモリ使用量の統計を取得する
current, peak = tracemalloc.get_traced_memory()
print(f"File length: {len(data)} characters")
print(f"Current memory usage: {current/1024/1024:.2f} MB")
print(f"Peak memory usage: {peak/1024/1024:.2f} MB")
## メモリの追跡を停止する
tracemalloc.stop()
if __name__ == "__main__":
test_single_string()
コードを追加したら、ファイルを保存します。次に、このスクリプトを実行するには、ターミナルを開き、以下のコマンドを実行します。
python3 /home/labex/project/memory_test1.py
スクリプトを実行すると、以下のような出力が表示されるはずです。
File length: 12361039 characters
Current memory usage: 11.80 MB
Peak memory usage: 23.58 MB
正確な数値はシステムによって異なる場合がありますが、一般的には、現在のメモリ使用量が約 12 MB、ピークメモリ使用量が約 24 MB となります。
方法 2: 文字列のリストとして保存する
次に、別のデータ保存方法をテストします。同じ /home/labex/project ディレクトリに memory_test2.py という名前の新しいファイルを作成します。エディタでこのファイルを開き、以下のコードを追加します。このコードは、ファイルを読み込み、各行を別々の文字列としてリストに保存し、メモリ使用量を測定します。
## memory_test2.py
import tracemalloc
def test_list_of_strings():
## メモリの追跡を開始する
tracemalloc.start()
## ファイルを文字列のリストとして読み込む(1 行ごとに 1 つの文字列)
with open('/home/labex/project/ctabus.csv') as f:
lines = f.readlines()
## メモリ使用量の統計を取得する
current, peak = tracemalloc.get_traced_memory()
print(f"Number of lines: {len(lines)}")
print(f"Current memory usage: {current/1024/1024:.2f} MB")
print(f"Peak memory usage: {peak/1024/1024:.2f} MB")
## メモリの追跡を停止する
tracemalloc.stop()
if __name__ == "__main__":
test_list_of_strings()
ファイルを保存し、ターミナルで以下のコマンドを使用してスクリプトを実行します。
python3 /home/labex/project/memory_test2.py
以下のような出力が表示されるはずです。
Number of lines: 577564
Current memory usage: 43.70 MB
Peak memory usage: 43.74 MB
データを単一の文字列として保存する前の方法と比較すると、メモリ使用量が大幅に増加していることに注意してください。これは、リスト内の各行が別々の Python 文字列オブジェクトであり、各オブジェクトには独自のメモリオーバーヘッドがあるためです。
メモリ使用量の違いを理解する
2 つのアプローチ間のメモリ使用量の違いは、Python プログラミングにおけるオブジェクトオーバーヘッドと呼ばれる重要な概念を示しています。データを文字列のリストとして保存する場合、各文字列は別々の Python オブジェクトです。各オブジェクトには、以下を含むいくつかの追加のメモリ要件があります。
- Python オブジェクトヘッダー(通常、オブジェクトごとに 16 - 24 バイト)。このヘッダーには、オブジェクトのタイプや参照カウントなどの情報が含まれています。
- 文字列自体の実際の表現で、文字列の文字を格納します。
- メモリアライメントパディング。これは、オブジェクトのメモリアドレスが効率的なアクセスのために適切にアライメントされるように追加される余分なスペースです。
一方、ファイルの内容全体を単一の文字列として保存する場合、オブジェクトは 1 つだけであり、したがってオーバーヘッドも 1 セットだけです。これにより、データの総サイズを考慮すると、メモリ効率が向上します。
大規模なデータセットを扱うプログラムを設計する際には、メモリ効率とデータのアクセス性の間のトレードオフを考慮する必要があります。場合によっては、データを文字列のリストとして保存するとアクセスが便利になることがありますが、メモリをより多く使用します。他の場合には、メモリ効率を優先して、データを単一の文字列として保存することがあります。
タプルを使った構造化データの操作
これまでは生のテキストデータの保存を扱ってきました。しかし、データ分析の際には、通常、データをより整理された構造化形式に変換する必要があります。これにより、様々な操作を行いやすくなり、データから洞察を得ることができます。このステップでは、csv モジュールを使ってデータをタプルのリストとして読み込む方法を学びます。タプルは、複数の値を保持できる Python のシンプルで便利なデータ構造です。
タプルを使ったリーダー関数の作成
/home/labex/project ディレクトリに readrides.py という名前の新しいファイルを作成しましょう。このファイルには、CSV ファイルからデータを読み込み、タプルのリストとして保存するコードが含まれます。
## readrides.py
import csv
import tracemalloc
def read_rides_as_tuples(filename):
'''
バスの乗車データをタプルのリストとして読み込む
'''
records = []
with open(filename) as f:
rows = csv.reader(f)
headings = next(rows) ## ヘッダーをスキップする
for row in rows:
route = row[0]
date = row[1]
daytype = row[2]
rides = int(row[3])
record = (route, date, daytype, rides)
records.append(record)
return records
if __name__ == '__main__':
tracemalloc.start()
rows = read_rides_as_tuples('/home/labex/project/ctabus.csv')
current, peak = tracemalloc.get_traced_memory()
print(f'Number of records: {len(rows)}')
print(f'First record: {rows[0]}')
print(f'Second record: {rows[1]}')
print(f'Memory Use: Current {current/1024/1024:.2f} MB, Peak {peak/1024/1024:.2f} MB')
このスクリプトは read_rides_as_tuples という関数を定義しています。以下はその処理の流れです。
filenameパラメータで指定された CSV ファイルを開きます。これにより、ファイル内のデータにアクセスできます。csvモジュールを使ってファイルの各行を解析します。csv.reader関数は、行を個々の値に分割するのに役立ちます。- 各行から 4 つのフィールド(路線、日付、曜日タイプ、乗車人数)を抽出します。これらのフィールドはデータ分析に重要です。
- 'rides' フィールドを整数に変換します。CSV ファイル内のデータは最初は文字列形式なので、計算には数値が必要です。
- これら 4 つの値を持つタプルを作成します。タプルは不変であり、一度作成されると値を変更することはできません。
- タプルを
recordsというリストに追加します。このリストは CSV ファイルのすべてのレコードを保持します。
では、スクリプトを実行しましょう。ターミナルを開き、以下のコマンドを入力します。
python3 /home/labex/project/readrides.py
以下のような出力が表示されるはずです。
Number of records: 577563
First record: ('3', '01/01/2001', 'U', 7354)
Second record: ('4', '01/01/2001', 'U', 9288)
Memory Use: Current 89.12 MB, Peak 89.15 MB
前の例と比較すると、メモリ使用量が増加していることに注意してください。これにはいくつかの理由があります。
- 現在、データを構造化形式(タプル)で保存しています。構造化データは通常、定義された組織構造を持つため、より多くのメモリを必要とします。
- タプル内の各値は別々の Python オブジェクトです。Python オブジェクトにはオーバーヘッドがあり、これがメモリ使用量の増加に寄与します。
- これらすべてのタプルを保持する追加のリスト構造があります。リストも要素を保存するためにメモリを占有します。
このアプローチの利点は、データが適切に構造化され、分析の準備ができていることです。各レコードの特定のフィールドにインデックスで簡単にアクセスできます。例えば:
## タプル要素へのアクセスの例(試すには readrides.py ファイルに追加する)
first_record = rows[0]
route = first_record[0]
date = first_record[1]
daytype = first_record[2]
rides = first_record[3]
print(f"Route: {route}, Date: {date}, Day type: {daytype}, Rides: {rides}")
ただし、数値インデックスでデータにアクセスすることは常に直感的ではありません。特に多数のフィールドを扱う場合、どのインデックスがどのフィールドに対応するかを覚えるのは難しいことがあります。次のステップでは、コードをより読みやすく保守しやすくする他のデータ構造を探索します。
異なるデータ構造の比較
Python では、データ構造を使用して関連するデータを整理し、保存します。データ構造は、構造化された方法でさまざまな種類の情報を保持するコンテナのようなものです。このステップでは、異なるデータ構造を比較し、それぞれがどれだけのメモリを使用するかを見ていきます。
/home/labex/project ディレクトリに compare_structures.py という名前の新しいファイルを作成しましょう。このファイルには、CSV ファイルからデータを読み込み、異なるデータ構造に保存するコードが含まれます。
## compare_structures.py
import csv
import tracemalloc
from collections import namedtuple
## 乗車データ用の名前付きタプルを定義する
RideRecord = namedtuple('RideRecord', ['route', 'date', 'daytype', 'rides'])
## 名前付きタプルは、フィールドに名前でアクセスできる軽量クラスです。
## タプルのようなものですが、名前付き属性を持っています。
## メモリ最適化のために __slots__ を持つクラスを定義する
class SlottedRideRecord:
__slots__ = ['route', 'date', 'daytype', 'rides']
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
## __slots__ を持つクラスは、メモリが最適化されたクラスです。
## インスタンス辞書を使用しないため、メモリを節約します。
## 乗車データ用の通常のクラスを定義する
class RegularRideRecord:
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
## 通常のクラスは、データを表現するオブジェクト指向の方法です。
## 名前付き属性を持ち、メソッドを持つことができます。
## データをタプルとして読み込む関数
def read_as_tuples(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## ヘッダーをスキップする
for row in rows:
record = (row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## この関数は、CSV ファイルからデータを読み込み、タプルとして保存します。
## タプルは不変のシーケンスであり、要素には数値インデックスでアクセスします。
## データを辞書として読み込む関数
def read_as_dicts(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## ヘッダーを取得する
for row in rows:
record = {
'route': row[0],
'date': row[1],
'daytype': row[2],
'rides': int(row[3])
}
records.append(record)
return records
## この関数は、CSV ファイルからデータを読み込み、辞書として保存します。
## 辞書はキーと値のペアを使用するため、要素に名前でアクセスできます。
## データを名前付きタプルとして読み込む関数
def read_as_named_tuples(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## ヘッダーをスキップする
for row in rows:
record = RideRecord(row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## この関数は、CSV ファイルからデータを読み込み、名前付きタプルとして保存します。
## 名前付きタプルは、タプルの効率性と名前によるアクセスの読みやすさを兼ね備えています。
## データを通常のクラスのインスタンスとして読み込む関数
def read_as_regular_classes(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## ヘッダーをスキップする
for row in rows:
record = RegularRideRecord(row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## この関数は、CSV ファイルからデータを読み込み、通常のクラスのインスタンスとして保存します。
## 通常のクラスを使用すると、データにメソッドを追加できます。
## データを __slots__ を持つクラスのインスタンスとして読み込む関数
def read_as_slotted_classes(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## ヘッダーをスキップする
for row in rows:
record = SlottedRideRecord(row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## この関数は、CSV ファイルからデータを読み込み、__slots__ を持つクラスのインスタンスとして保存します。
## __slots__ を持つクラスはメモリが最適化されており、名前によるアクセスも提供します。
## メモリ使用量を測定する関数
def measure_memory(func, filename):
tracemalloc.start()
records = func(filename)
current, peak = tracemalloc.get_traced_memory()
## 各データ構造の使用方法を示す
first_record = records[0]
if func.__name__ == 'read_as_tuples':
route, date, daytype, rides = first_record
elif func.__name__ == 'read_as_dicts':
route = first_record['route']
date = first_record['date']
daytype = first_record['daytype']
rides = first_record['rides']
else: ## 名前付きタプルとクラス
route = first_record.route
date = first_record.date
daytype = first_record.daytype
rides = first_record.rides
print(f"Structure type: {func.__name__}")
print(f"Record count: {len(records)}")
print(f"Example access: Route={route}, Date={date}, Rides={rides}")
print(f"Current memory: {current/1024/1024:.2f} MB")
print(f"Peak memory: {peak/1024/1024:.2f} MB")
print("-" * 50)
tracemalloc.stop()
return current
if __name__ == "__main__":
filename = '/home/labex/project/ctabus.csv'
## すべてのメモリテストを実行する
print("Memory usage comparison for different data structures:\n")
results = []
for reader_func in [
read_as_tuples,
read_as_dicts,
read_as_named_tuples,
read_as_regular_classes,
read_as_slotted_classes
]:
memory = measure_memory(reader_func, filename)
results.append((reader_func.__name__, memory))
## メモリ使用量でソートする(少ない順)
results.sort(key=lambda x: x[1])
print("\nRanking by memory efficiency (most efficient first):")
for i, (name, memory) in enumerate(results, 1):
print(f"{i}. {name}: {memory/1024/1024:.2f} MB")
比較結果を確認するには、スクリプトを実行します。
python3 /home/labex/project/compare_structures.py
出力には、各データ構造のメモリ使用量と、メモリ効率の高い順から低い順のランキングが表示されます。
異なるデータ構造の理解
タプル:
- タプルは軽量で不変のシーケンスです。つまり、タプルを作成した後は、その要素を変更することができません。
- タプルの要素には、
record[0]、record[1]などの数値インデックスでアクセスします。 - 構造がシンプルなため、メモリ効率が非常に高いです。
- ただし、各要素のインデックスを覚える必要があるため、読みやすさが劣ることがあります。
辞書:
- 辞書はキーと値のペアを使用するため、要素に名前でアクセスできます。
- 例えば、
record['route']、record['date']などを使用できるため、読みやすさが高いです。 - キーと値のペアを保存するためにハッシュテーブルのオーバーヘッドがあるため、メモリ使用量が多くなります。
- フィールドの追加や削除が容易なため、柔軟性が高いです。
名前付きタプル:
- 名前付きタプルは、タプルの効率性と要素に名前でアクセスする機能を兼ね備えています。
record.route、record.dateなどのドット表記を使用して要素にアクセスできます。- 通常のタプルと同様に不変です。
- 辞書よりもメモリ効率が高いです。
通常のクラス:
- 通常のクラスはオブジェクト指向のアプローチに従い、名前付き属性を持っています。
record.route、record.dateなどのドット表記を使用して属性にアクセスできます。- 通常のクラスには、振る舞いを定義するメソッドを追加できます。
- 各インスタンスに属性を保存するためのインスタンス辞書があるため、メモリ使用量が多くなります。
**slots を持つクラス**:
__slots__を持つクラスはメモリが最適化されたクラスです。インスタンス辞書を使用しないため、メモリを節約します。record.route、record.dateなどの名前による属性へのアクセスを提供します。- オブジェクトを作成した後は、新しい属性の追加が制限されます。
- 通常のクラスよりもメモリ効率が高いです。
各アプローチの使用時期
- タプル: メモリが重要な要素であり、データに単純なインデックスアクセスのみが必要な場合に使用します。
- 辞書: データのフィールドが変化する可能性があるなど、柔軟性が必要な場合に使用します。
- 名前付きタプル: 読みやすさとメモリ効率の両方が必要な場合に使用します。
- 通常のクラス: データに振る舞い(メソッド)を追加する必要がある場合に使用します。
- **slots を持つクラス**: 振る舞いと最大限のメモリ効率が必要な場合に使用します。
必要に応じて適切なデータ構造を選択することで、特に大規模なデータセットを扱う場合に、Python プログラムのパフォーマンスとメモリ使用量を大幅に改善することができます。
まとめ
この実験では、Python でレコードを表現するさまざまな方法を学び、それらのメモリ効率を分析しました。まず、基本的な CSV データセットの構造を理解し、生のテキスト保存方法を比較しました。次に、タプルを使って構造化データを操作し、タプル、辞書、名前付きタプル、通常のクラス、および __slots__ を持つクラスという 5 つの異なるデータ構造を実装しました。
重要なポイントとして、異なるデータ構造はメモリ効率、読みやすさ、および機能性の間でトレードオフを提供します。Python のオブジェクトのオーバーヘッドは、大規模なデータセットのメモリ使用量に大きな影響を与え、データ構造の選択はメモリ消費量に大きく影響することがあります。名前付きタプルと __slots__ を持つクラスは、メモリ効率とコードの読みやすさの間の良い妥協点です。これらの概念は、データ処理における Python 開発者にとって、特にメモリ効率が重要な大規模なデータセットを扱う際に、非常に価値があります。