レコードを表現するさまざまな方法

PythonPythonBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

この実験では、Python で大規模なデータセットを格納するためのメモリ効率の良い方法を学びます。また、タプル、辞書、クラス、名前付きタプルなど、レコードを表現するさまざまな方法を発見します。

さらに、さまざまなデータ構造のメモリ使用量を比較します。これらの構造間のトレードオフを理解することは、データ分析を行う Python ユーザーにとって重要です。なぜなら、これによりコードの最適化に役立つからです。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/DataStructuresGroup(["Data Structures"]) python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python/DataStructuresGroup -.-> python/tuples("Tuples") python/DataStructuresGroup -.-> python/dictionaries("Dictionaries") python/FileHandlingGroup -.-> python/file_opening_closing("Opening and Closing Files") python/FileHandlingGroup -.-> python/file_reading_writing("Reading and Writing Files") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/FileHandlingGroup -.-> python/with_statement("Using with Statement") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/tuples -.-> lab-132428{{"レコードを表現するさまざまな方法"}} python/dictionaries -.-> lab-132428{{"レコードを表現するさまざまな方法"}} python/file_opening_closing -.-> lab-132428{{"レコードを表現するさまざまな方法"}} python/file_reading_writing -.-> lab-132428{{"レコードを表現するさまざまな方法"}} python/file_operations -.-> lab-132428{{"レコードを表現するさまざまな方法"}} python/with_statement -.-> lab-132428{{"レコードを表現するさまざまな方法"}} python/data_collections -.-> lab-132428{{"レコードを表現するさまざまな方法"}} end

データセットの探索

まずは、今回取り扱うデータセットを詳しく見ていきましょう。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 列のデータがあることがわかります。各列が表す内容を分解してみましょう。

  1. route:これはバス路線の名前または番号です。データセットの最初の列 (列 0) です。
  2. date:MM/DD/YYYY 形式の日付文字列です。これは 2 番目の列 (列 1) です。
  3. daytype:曜日タイプのコードで、3 番目の列 (列 2) です。
    • U = 日曜日/祝日
    • A = 土曜日
    • W = 平日
  4. 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 によるメモリ割り当てを追跡できるため非常に便利です。これを使用することで、データ保存方法がどれだけのメモリを消費しているかを確認できます。

方法 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 オブジェクトです。各オブジェクトには、以下を含むいくつかの追加のメモリ要件があります。

  1. Python オブジェクトヘッダー(通常、オブジェクトごとに 16 - 24 バイト)。このヘッダーには、オブジェクトのタイプや参照カウントなどの情報が含まれています。
  2. 文字列自体の実際の表現で、文字列の文字を格納します。
  3. メモリアライメントパディング。これは、オブジェクトのメモリアドレスが効率的なアクセスのために適切にアライメントされるように追加される余分なスペースです。

一方、ファイルの内容全体を単一の文字列として保存する場合、オブジェクトは 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 という関数を定義しています。以下はその処理の流れです。

  1. filename パラメータで指定された CSV ファイルを開きます。これにより、ファイル内のデータにアクセスできます。
  2. csv モジュールを使ってファイルの各行を解析します。csv.reader 関数は、行を個々の値に分割するのに役立ちます。
  3. 各行から 4 つのフィールド(路線、日付、曜日タイプ、乗車人数)を抽出します。これらのフィールドはデータ分析に重要です。
  4. 'rides' フィールドを整数に変換します。CSV ファイル内のデータは最初は文字列形式なので、計算には数値が必要です。
  5. これら 4 つの値を持つタプルを作成します。タプルは不変であり、一度作成されると値を変更することはできません。
  6. タプルを 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

前の例と比較すると、メモリ使用量が増加していることに注意してください。これにはいくつかの理由があります。

  1. 現在、データを構造化形式(タプル)で保存しています。構造化データは通常、定義された組織構造を持つため、より多くのメモリを必要とします。
  2. タプル内の各値は別々の Python オブジェクトです。Python オブジェクトにはオーバーヘッドがあり、これがメモリ使用量の増加に寄与します。
  3. これらすべてのタプルを保持する追加のリスト構造があります。リストも要素を保存するためにメモリを占有します。

このアプローチの利点は、データが適切に構造化され、分析の準備ができていることです。各レコードの特定のフィールドにインデックスで簡単にアクセスできます。例えば:

## タプル要素へのアクセスの例(試すには 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

出力には、各データ構造のメモリ使用量と、メモリ効率の高い順から低い順のランキングが表示されます。

異なるデータ構造の理解

  1. タプル:

    • タプルは軽量で不変のシーケンスです。つまり、タプルを作成した後は、その要素を変更することができません。
    • タプルの要素には、record[0]record[1] などの数値インデックスでアクセスします。
    • 構造がシンプルなため、メモリ効率が非常に高いです。
    • ただし、各要素のインデックスを覚える必要があるため、読みやすさが劣ることがあります。
  2. 辞書:

    • 辞書はキーと値のペアを使用するため、要素に名前でアクセスできます。
    • 例えば、record['route']record['date'] などを使用できるため、読みやすさが高いです。
    • キーと値のペアを保存するためにハッシュテーブルのオーバーヘッドがあるため、メモリ使用量が多くなります。
    • フィールドの追加や削除が容易なため、柔軟性が高いです。
  3. 名前付きタプル:

    • 名前付きタプルは、タプルの効率性と要素に名前でアクセスする機能を兼ね備えています。
    • record.routerecord.date などのドット表記を使用して要素にアクセスできます。
    • 通常のタプルと同様に不変です。
    • 辞書よりもメモリ効率が高いです。
  4. 通常のクラス:

    • 通常のクラスはオブジェクト指向のアプローチに従い、名前付き属性を持っています。
    • record.routerecord.date などのドット表記を使用して属性にアクセスできます。
    • 通常のクラスには、振る舞いを定義するメソッドを追加できます。
    • 各インスタンスに属性を保存するためのインスタンス辞書があるため、メモリ使用量が多くなります。
  5. **slots を持つクラス**:

    • __slots__ を持つクラスはメモリが最適化されたクラスです。インスタンス辞書を使用しないため、メモリを節約します。
    • record.routerecord.date などの名前による属性へのアクセスを提供します。
    • オブジェクトを作成した後は、新しい属性の追加が制限されます。
    • 通常のクラスよりもメモリ効率が高いです。

各アプローチの使用時期

  • タプル: メモリが重要な要素であり、データに単純なインデックスアクセスのみが必要な場合に使用します。
  • 辞書: データのフィールドが変化する可能性があるなど、柔軟性が必要な場合に使用します。
  • 名前付きタプル: 読みやすさとメモリ効率の両方が必要な場合に使用します。
  • 通常のクラス: データに振る舞い(メソッド)を追加する必要がある場合に使用します。
  • **slots を持つクラス**: 振る舞いと最大限のメモリ効率が必要な場合に使用します。

必要に応じて適切なデータ構造を選択することで、特に大規模なデータセットを扱う場合に、Python プログラムのパフォーマンスとメモリ使用量を大幅に改善することができます。

✨ 解答を確認して練習

まとめ

この実験では、Python でレコードを表現するさまざまな方法を学び、それらのメモリ効率を分析しました。まず、基本的な CSV データセットの構造を理解し、生のテキスト保存方法を比較しました。次に、タプルを使って構造化データを操作し、タプル、辞書、名前付きタプル、通常のクラス、および __slots__ を持つクラスという 5 つの異なるデータ構造を実装しました。

重要なポイントとして、異なるデータ構造はメモリ効率、読みやすさ、および機能性の間でトレードオフを提供します。Python のオブジェクトのオーバーヘッドは、大規模なデータセットのメモリ使用量に大きな影響を与え、データ構造の選択はメモリ消費量に大きく影響することがあります。名前付きタプルと __slots__ を持つクラスは、メモリ効率とコードの読みやすさの間の良い妥協点です。これらの概念は、データ処理における Python 開発者にとって、特にメモリ効率が重要な大規模なデータセットを扱う際に、非常に価値があります。