2026年9月18日金曜日

技術メモ QGIS画面での分類結果データの書き出し方法

 Technical Note: Exporting Classification Results in QGIS


QGIS allows feature data to be displayed using various classification methods. I explored a way to export these classification results—represented by values ​​such as 1 through 10, depending on the number of classes—into a new data field. I created a convenient Python script that enables the export of this data regardless of the classification method or number of classes used in the QGIS display.


QGISでは地物データを多用な方法で分類表示できます。この分類表示結果を区分数に応じて1~10などの数値データで新たなフィールドに書き出す方法を検討しました。QGIS画面で表示したどのような分類方法、分類数でもデータ書き出しができる便利なPythonスクリプトを作成しました。

1 フィールド計算機を使った方法 その1

1-1 等間隔分類

サンプルデータ(0~90の範囲内にある小数点のある数値、レコード数3024、フィールドkousaに格納されている。)

フィールドkousaについて、シンボロジ→連続値による定義→フィールド入力→等間隔分類(分類数10)で画面上で分類しました。


画面上での分類結果

この画面上での分類結果を、数値の小さい方から1~10の数字を割り当てた区分データに変換し、それを新たなフィールドkousakubun3に出力する式(例)とフィールド計算機画面は次の通りです。

式(例)

CASE

    WHEN "kousa" IS NULL THEN NULL

    WHEN maximum("kousa") = minimum("kousa") THEN 1

    ELSE

        -- kousaの値を1.0〜10.999の範囲に線形変換し、floorで整数化(1〜10になる)

        floor(scale_linear("kousa", minimum("kousa"), maximum("kousa"), 1, 10.9999))END


フィールド計算機画面

これで無事等間隔分類を1~10の数値データに変換して、新たなフィールドに書き込むことができました。

1-2 等量分類

等量分類についての式は複雑になり過ぎて、ギブアップしました。

1-3 感想

等間隔分類はなんとかフィールド計算機で再現できましたが、等量分類はギブアップで、他の分類(丸め間隔、固定間隔、対数スケール、標準偏差、自然分類)は思考の意欲が生まれませんでした。

2 フィールド計算機を使った方法 その2

分類のアルゴリズムを再現するのではなく、QGIS操作で生じた分類結果そのものをフィールド計算機にとりこむ方法があります。

例えばシンボロジの分類結果が仮に、

1 :  2.14 ~  5.72

2 :  5.72 ~  8.31

3 :  8.31 ~ 11.45

...

10: 47.21 ~ 88.53

となった場合、フィールド計算機では、

CASE

WHEN "kousa" IS NULL THEN NULL

WHEN "kousa" <= 5.72 THEN 1

WHEN "kousa" <= 8.31 THEN 2

WHEN "kousa" <= 11.45 THEN 3

...

WHEN "kousa" <= 47.21 THEN 9

ELSE 10

END

とすれば、画面上の分類と同じ分類を属性として保存できます。

この方法なら、どのような分類でも、さらに一部を意図的に変更したものでも、対応できます。

ただし、分類の数値を手でひろい入力する必要があり、AI時代にふさわしい方法とは言えません。

3 Pythonスクリプトを使った方法

次のPythonスクリプトをプラグイン→Pythonコンソール→エディタの表示でひらくエディタ画面にはりつけて走らせれば、どのような分類でも1~10などの数値データを新たなフィールドのレコードに書き込むことができます。この方法も分類のアルゴリズムを再現するのではなく、QGIS操作で生じた分類結果そのものを新フィールドにとりこむ方法です。

このPythonスクリプトは、レイヤ名称aaa、対象フィールド名称bbb、新フィールド名称bbb_class、分類数10の場合で記述されていますが、それぞれ変更可能です。ChatGPT支援で作成しました。


from qgis.core import (
    QgsProject,
    QgsGraduatedSymbolRenderer,
    QgsField
)
from qgis.PyQt.QtCore import QVariant

# ============================================================
# 設定
# ============================================================

LAYER_NAME = "aaa"
VALUE_FIELD = "bbb"
CLASS_FIELD = "bbb_class"

# ============================================================
# レイヤ取得
# ============================================================

layers = QgsProject.instance().mapLayersByName(LAYER_NAME)

if not layers:
    raise Exception(f"レイヤ '{LAYER_NAME}' が見つかりません。")

layer = layers[0]

# bbb の存在確認
if layer.fields().indexFromName(VALUE_FIELD) == -1:
    raise Exception(f"フィールド '{VALUE_FIELD}' が見つかりません。")

# ============================================================
# 現在のシンボロジを取得
# ============================================================

renderer = layer.renderer()

if not isinstance(renderer, QgsGraduatedSymbolRenderer):
    raise Exception(
        "現在のシンボロジが「連続値による定義」ではありません。"
    )

# 分類に使用しているフィールドを確認
class_attribute = renderer.classAttribute()

if class_attribute != VALUE_FIELD:
    raise Exception(
        f"現在の分類フィールドは '{class_attribute}' です。"
        f"'{VALUE_FIELD}' ではありません。"
    )

# 現在画面で設定されている分類範囲を取得
ranges = list(renderer.ranges())

if len(ranges) != 10:
    raise Exception(
        f"現在の分類数は {len(ranges)} です。"
        "設定分類にしてから実行してください。"
    )

# ============================================================
# 分類範囲を確認表示
# ============================================================

print("現在の分類範囲")
print("------------------------------")

for i, r in enumerate(ranges, start=1):
    print(
        f"Class {i}: "
        f"{r.lowerValue()} ~ {r.upperValue()}"
    )

print("------------------------------")

# ============================================================
# bbb_class フィールドを作成
# ============================================================

class_idx = layer.fields().indexFromName(CLASS_FIELD)

if class_idx == -1:

    if not layer.isEditable():
        if not layer.startEditing():
            raise Exception(
                "レイヤを編集モードにできませんでした。"
            )

    success = layer.addAttribute(
        QgsField(CLASS_FIELD, QVariant.Int)
    )

    if not success:
        raise Exception(
            f"フィールド '{CLASS_FIELD}' を作成できませんでした。"
        )

    layer.updateFields()

    class_idx = layer.fields().indexFromName(CLASS_FIELD)

else:

    if not layer.isEditable():
        if not layer.startEditing():
            raise Exception(
                "レイヤを編集モードにできませんでした。"
            )

# ============================================================
# 各地物を現在のシンボロジと同じ区分に分類
# ============================================================

count = 0
null_count = 0
unclassified_count = 0

for feature in layer.getFeatures():

    value = feature[VALUE_FIELD]

    # NULLの場合
    if value is None or value == QVariant():
        layer.changeAttributeValue(
            feature.id(),
            class_idx,
            None
        )
        null_count += 1
        continue

    try:
        value = float(value)
    except (TypeError, ValueError):
        layer.changeAttributeValue(
            feature.id(),
            class_idx,
            None
        )
        unclassified_count += 1
        continue

    class_number = None

    # QGISの graduated renderer と同様に
    # 上から順番に該当する範囲を探す
    for i, r in enumerate(ranges, start=1):

        if (
            r.lowerValue() <= value
            and
            value <= r.upperValue()
        ):
            class_number = i
            break

    if class_number is not None:

        layer.changeAttributeValue(
            feature.id(),
            class_idx,
            class_number
        )

        count += 1

    else:

        layer.changeAttributeValue(
            feature.id(),
            class_idx,
            None
        )

        unclassified_count += 1

# ============================================================
# 結果
# ============================================================

print("")
print("処理完了")
print("==============================")
print(f"分類した地物数 : {count}")
print(f"NULL           : {null_count}")
print(f"分類不能       : {unclassified_count}")
print("==============================")
print("")
print(
    "属性テーブルの新フィールドに "
    "分類番号を書き込みました。"
)
print(
    "内容を確認して問題なければ、"
    "レイヤの編集内容を保存してください。"
)

4 感想


レイヤの属性テーブル 例

今回作成Pythonスクリプトでどのような分類、分類数でも、新しいフィールドのレコードに分類を指標する1~10など数値を書き込むことができるようになりました。画面の分類結果をデータとして残せるようになりました。画面で多種分類方法をシミュレーションして(分布を確認して)、適切なもののデータを残せるのでとても便利です。

0 件のコメント:

コメントを投稿