はじめに
この実験では、Python で大規模なデータセットを格納するためのメモリ効率の良い方法を学びます。また、タプル、辞書、クラス、名前付きタプルなど、レコードを表現するさまざまな方法を発見します。
さらに、さまざまなデータ構造のメモリ使用量を比較します。これらの構造間のトレードオフを理解することは、データ分析を行う Python ユーザーにとって重要です。なぜなら、これによりコードの最適化に役立つからです。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
この実験では、Python で大規模なデータセットを格納するためのメモリ効率の良い方法を学びます。また、タプル、辞書、クラス、名前付きタプルなど、レコードを表現するさまざまな方法を発見します。
さらに、さまざまなデータ構造のメモリ使用量を比較します。これらの構造間のトレードオフを理解することは、データ分析を行う Python ユーザーにとって重要です。なぜなら、これによりコードの最適化に役立つからです。
まずは、今回取り扱うデータセットを詳しく見ていきましょう。ctabus.csv
ファイルは CSV (Comma-Separated Values、カンマ区切り値) ファイルです。CSV ファイルは表形式のデータを格納する一般的な方法で、各行が 1 つのレコードを表し、レコード内の値はカンマで区切られています。この特定のファイルには、2001 年 1 月 1 日から 2013 年 8 月 31 日までのシカゴ交通局 (Chicago Transit Authority, CTA) バスシステムの日々の乗車人数データが含まれています。
このファイルの構造を理解するために、まずは中身をのぞいてみましょう。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
:これはバス路線の名前または番号です。データセットの最初の列 (列 0) です。date
:MM/DD/YYYY 形式の日付文字列です。これは 2 番目の列 (列 1) です。daytype
:曜日タイプのコードで、3 番目の列 (列 2) です。
rides
:この列には、整数として乗車人数の合計が記録されています。4 番目の列 (列 3) です。rides
列は、特定の日に特定の路線のバスに乗車した人数を示します。たとえば、上記の出力から、2001 年 1 月 1 日に 3 番のバスに 7,354 人が乗車したことがわかります。
では、ファイルに何行あるかを調べてみましょう。行数を知ることで、データセットのサイズの目安がつきます。以下の 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 によるメモリ割り当てを追跡できるため非常に便利です。これを使用することで、データ保存方法がどれだけのメモリを消費しているかを確認できます。
まず、新しい 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 となります。
次に、別のデータ保存方法をテストします。同じ /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 オブジェクトです。各オブジェクトには、以下を含むいくつかの追加のメモリ要件があります。
一方、ファイルの内容全体を単一の文字列として保存する場合、オブジェクトは 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
関数は、行を個々の値に分割するのに役立ちます。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
前の例と比較すると、メモリ使用量が増加していることに注意してください。これにはいくつかの理由があります。
このアプローチの利点は、データが適切に構造化され、分析の準備ができていることです。各レコードの特定のフィールドにインデックスで簡単にアクセスできます。例えば:
## タプル要素へのアクセスの例(試すには 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
などの名前による属性へのアクセスを提供します。必要に応じて適切なデータ構造を選択することで、特に大規模なデータセットを扱う場合に、Python プログラムのパフォーマンスとメモリ使用量を大幅に改善することができます。
この実験では、Python でレコードを表現するさまざまな方法を学び、それらのメモリ効率を分析しました。まず、基本的な CSV データセットの構造を理解し、生のテキスト保存方法を比較しました。次に、タプルを使って構造化データを操作し、タプル、辞書、名前付きタプル、通常のクラス、および __slots__
を持つクラスという 5 つの異なるデータ構造を実装しました。
重要なポイントとして、異なるデータ構造はメモリ効率、読みやすさ、および機能性の間でトレードオフを提供します。Python のオブジェクトのオーバーヘッドは、大規模なデータセットのメモリ使用量に大きな影響を与え、データ構造の選択はメモリ消費量に大きく影響することがあります。名前付きタプルと __slots__
を持つクラスは、メモリ効率とコードの読みやすさの間の良い妥協点です。これらの概念は、データ処理における Python 開発者にとって、特にメモリ効率が重要な大規模なデータセットを扱う際に、非常に価値があります。