Создайте объекты MultiPolygon из типа структуры и столбцов list[list[list[f64]]] с помощью Polars

Я загрузил набор данных «Зоны такси Нью-Йорка» (загружен из SODA Api и сохранен как файл json, а не GeoJson или Shapefile). Набор данных довольно мал, поэтому я использую всю включенную информацию. Для удобства поста представляю первые 2 строки набора данных:

  • исходный (со значением типа структуры the_geom).
  • набор данных после распаковки типа структуры с помощью команды unnest() в полярах. --обновлено командой write_ndjson()

Исходный набор данных:

Ниже набора данных после применения unnest() и выбора некоторых столбцов и первых двух строк.

Вы можете импортировать данные, используя поляры, с помощью следующей команды

import polars as pl
poc = pl.read_json("./data.json"))

Меня интересуют мультиполигоны. На самом деле я пытаюсь пересчитать shape_area, используя мультиполигон и wkt (представление хорошо известного текста) - метод, используемый модулем shapely.

До сих пор я использовал столбец coordinates и преобразовывал его в объект MultiPolygon(), читаемый модулем Shapely.

def flatten_list_of_lists(data):
    return [subitem3 for subitem1 in data for subitem2 in subitem1 for subitem3 in subitem2]

Функция принимает на вход объект list[list[list[list[f64]]]] и преобразуется в объект list[list[f64]].

flattened_lists = [flatten_list_of_lists(row) for row in poc["coordinates"].to_list()]

print(flattened_lists)

[[[-74.18445299999996, 40.694995999999904], [-74.18448899999999, 40.69509499999987], [-74.18449799999996, 40.69518499999987], [-7], 4.18438099999997, 40.69587799999989], [-74.18428199999994, 40.6962109999999], [-74.18402099999997, 40.69707499999989]...

Затем я использую функцию ниже, которая применяет конкатенацию строк и в основном:

  • Преобразует объект list[list[f64]] в String.
  • Добавляет ключевое слово MultiPolygon перед строкой.
  • Заменяет '[' и ']' на '('., ')' соответственно.
def polygon_to_wkt(polygon):
    # Convert each coordinate pair to a string formatted as "lon lat"
    coordinates_str = ", ".join(f"{lon} {lat}" for lon, lat in polygon)
    # Format into a WKT Multipolygon string (each polygon as a single polygon multipolygon)
    return f"""MULTIPOLYGON ((({coordinates_str})))"""

formatted_wkt = [polygon_to_wkt(polygon) for polygon in flattened_lists]
poc = poc.with_columns(pl.Series("WKT", formatted_wkt))

Наконец, я использую метод wkt.loads("MultiPolygon ((()))").area для вычисления площади формы объекта Multipolygon.

def convert_to_shapely_area(wkt_str):
    try:
        return wkt.loads(wkt_str).area
    except Exception as e:
        print("Error converting to WKT:", e)
        return None
poc = poc.with_columns(
    pl.col("WKT").map_elements(convert_to_shapely_area).alias("shapely_geometry")
)
print(poc.head())

Хотя для первой фигуры WKT правильно возвращает площадь объекта, а для второй MultiPolygon методы возвращают следующую ошибку:

IllegalArgumentException: точки LinearRing не образуют замкнутую строку

Между двумя строками я заметил, что мультиполигон аэропорта Ньюарка представляет собой непрерывный объект координат list[list[f64]]]. Принимая во внимание, что залив Ямайка имеет несколько элементов подсписков [list[list[f64]]] (проверьте координаты столбца, чтобы убедиться в этом). Также скриншот ниже подтверждает это утверждение.

Таким образом, есть ли способ объединить мультиполигоны залива Ямайка в один объект GEOmetric перед применением WKT?

P.S. Многие решения на GitHub используют файл формы, но я хотел бы регулярно автоматически повторно загружать зоны Нью-Йорка с помощью SODA API.

Чтобы загрузить необработанный файл .json из SODA API (опустите logger_object, давайте представим, что это print())

import requests
params = {
        "$limit": geospatial_batch_size, #i.e. 50_000
        "$$app_token": config.get("api-settings", "app_token")
    }
    response = requests.get(api_url, params=params)
    if response.status_code == 200:
        data = response.json()
        if not data:
            logger_object.info("No data found, please check the API connection.")
            sys.exit()
        with open("{0}/nyc_zone_districts_data.json".format(geospatial_data_storage), "w") as f:
            json.dump(data, f, indent=4)
    else:
        logger_object.error("API request failed.")
        logger_object.error("Error: {0}".format(response.status_code))
        logger_object.error(response.text)

Вы использовали .write_json() для создания этих файлов? Возможно, вместо этого вы можете использовать .write_ndjson()? В настоящее время я получаю один столбец с именем columns, который представляет собой список структур, и из-за принудительного применения схемы множество данных удаляется/обнуляется.

jqurious 04.05.2024 12:32

Да, я использовал метод write_json(). Исходный набор данных имеет типы структур. Второй набор данных (второй маркер в SO) имеет образец unnest() с list[list[list.. Вот почему я предложил использовать второй набор данных, а не исходный. Я только что опубликовал оригинал, чтобы дать представление о том, как данные загружаются из SODA Api.

NikSp 04.05.2024 12:34

Да, но если вы попробуете read_json любой файл, вы получите один list[struct[4]] столбец с именем columns.

jqurious 04.05.2024 12:38

Хм, это странно. И вы предлагаете использовать write_ndjson()? Позвольте мне попробовать и обновить ссылку на набор данных.

NikSp 04.05.2024 12:40
write_json добавляет метаданные Polars. write_ndjson — это один из подходов, который производит только необработанные данные. (Я загрузил исходный файл такси из API, но Polars вызывает PanicException — думаю, из-за пустых ключей "format": {})
jqurious 04.05.2024 12:42

Я обновил вопрос, добавив вызов SODA API, на случай, если вы задумаетесь, как я загрузил необработанный файл. Позвольте мне сейчас проверить ndjson() в полярах.

NikSp 04.05.2024 12:46

@jqurious Загрузил новый путь к GoogleDrive.

NikSp 04.05.2024 12:49

Это работает — я могу загрузить data_unnseted.json, как и ожидалось, спасибо. (Может быть, вы хотите сделать то же самое для data.json)

jqurious 04.05.2024 12:53

Я буду благодарен за комментарий. Исходный набор данных имеет формат типа структуры, который не идеален для манипулирования shapely.

NikSp 04.05.2024 12:59

Я не полностью следую вашему вводу или ожидаемому результату, но что касается конструкции мультиполигона + площади, вы можете просто использовать from shapely.geometry import shape;[shape(x).area for x in poc["the_geom"]] или poc["the_geom"].map_elements(lambda x: shape(x).area) сразу после прочтения исходного json. Нет необходимости в дополнительной обработке строк.

Timeless 04.05.2024 14:21

Подход @Timeless мне подходит - возможно, вы сможете подтвердить, и они добавят это в качестве ответа?

jqurious 06.05.2024 11:10
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
11
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В конце концов я нашел решение этой проблемы. Как уже описано, я выравниваю список координат (т.н. точек). Точка в геопространственных данных — это координата (x,y). Таким образом, MultiPolygon — это комбинация нескольких точек.

def flatten_list_of_lists(data) -> list:
    return [subitem2 for subitem1 in data for subitem2 in subitem1]

, эта вышеупомянутая функция важна, поскольку объект data, используемый в качестве входного аргумента, имеет тип [list[list[list[f64]]]], а MultiPolygon имеет определенный уровень мощности для каждой точки.

Затем я преобразую его плоский список в объект MultiPolygon, используя shapely

from shapely.geometry import Polygon, MultiPolygon, Point
def transform_polygons_to_multipolygons(flat_list:list) -> list:
    return [ MultiPolygon( [Polygon(coord) for coord in polygon]).wkt for polygon in  flat_list]

Вы заметите, что после создания каждого объекта MultiPolygon я сохраняю его в формате WKT (то есть в виде строки).

Наконец, я вычисляю площадь мультиполигона

from shapely import wkt
def compute_geo_areas(multipolygons:MultiPolygon) -> float:
    return wkt.loads(multipolygons).area

Окончательный код

flattened_lists:list = [flatten_list_of_lists(row) for row in df_geo["coordinates"].to_list()]
multipolygons:list = transform_polygons_to_multipolygons(flattened_lists)

df_geo = df_geo.with_columns(pl.Series("multipolygons", multipolygons)) \
.with_columns(polygon_area=pl.col("multipolygons").map_elements(compute_geo_areas)) \
.drop("coordinates")

, где df_geo — фрейм данных Polars, загруженный из файла JSON, прикрепленного к вопросу SO.

Другие вопросы по теме