[{"content":"\r«Не все панки пришли к единому мнению о том, как нужно поддерживать других или каким образом можно что-то изменить во внешнем мире к лучшему. Но все согласны, что это необходимо делать.»\n― Крейг О\u0026#39;Хара, «Философия панка: больше, чем шум» Тему для второй заметки в рубрику «очумелые ручки» долго выбирать не пришлось — после прочтения первой, от одного из бета сигма-тестеров нашего блога поступил вопрос: «А как в Python можно получить набор изолиний из грида для последующего использования их (изолиний) в геоинформационных системах по типу ArcGIS QGIS? Очень нада!!!»\nНаш бродяга, вроде как загорелся идеей автоматизации процесса оформления отчетной графики в QGIS. Что ж, похвально! Как говорится, чем меньше времени мы тратим на рутину, тем больше времени остается на секс, пиво и рок-н-ролл творческую работу и эксперименты. Тем более, что тема действительно актуальная, в связи с последними требованиями к предоставляемым отчетным материалам в РГФ. Так давайте же убьем сразу двух зайцев: поможем братику и попутно разберем некоторые дополнительные типы пространственных данных, о которых мы не успели упомянуть в прошлый раз.\nLet\u0026rsquo;s get started! Подгрузим для начала нашу поверхность, чтобы было с чем работать. Это мы с вами уже умеем. В качестве подопытного образца будем использовать карту изохрон из предыдущего набора данных:\n1 2 3 4 5 6 7 8 9 10 import numpy as np import xarray as xr from zmapio import ZMAPGrid import matplotlib.pyplot as plt time_grid = ZMAPGrid(r\u0026#39;data\\surfaces\\P1ar_time.dat\u0026#39;) time_grid_xr = xr.DataArray(time_grid.z_values.T, name=\u0026#39;time\u0026#39;, coords=[time_grid.y_values[:, 0], time_grid.x_values[0]], dims=[\u0026#39;y\u0026#39;, \u0026#39;x\u0026#39;]) Визуализировать двумерный массив данных (грид) с помощью библиотеки Matplotlib можно несколькими способами:\nВизуализация двумерного массива данных в виде растрового изображения — функция imshow библиотеки Matplotlib или встроенная в библиотеку Xarray функция plot (под капотом она тоже использует функцию imshow библиотеки Matplotlib) Визуализация карты в виде контурных графиков с заливкой по заданным интервалам (векторная графика) с помощью функции contourf Визуализация изолиний — функция contour Комбинирование различных вариантов визуализации на одном графике только приветствуется. Совместим контурный график и изолинии:\n1 2 3 4 5 6 7 8 fig, ax = plt.subplots() collec_poly = time_grid_xr.plot.contourf(ax=ax, cmap=\u0026#39;Spectral_r\u0026#39;, levels = np.arange(-1700, -1900, -10)) isoline = time_grid_xr.plot.contour(colors=\u0026#39;black\u0026#39;, ax=ax, linewidths=0.5, levels = np.arange(-1700, -1900, -10), linestyles=\u0026#39;solid\u0026#39;) ax.axis(\u0026#39;scaled\u0026#39;) ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) На вход функциям contour и contourf, необходимо передать одномерный массив уровней levels. Именно он определяет шаг между изолиниями и уровни закраски для контурного графика. Я создал его с помощью функции arange из библиотеки numpy, задав равномерный шаг в 10 миллисекунд для интервала между граничными значениями массива от -1700 до -1900 мс. Граничные значения уровней я задал вручную, хотя лучше было бы получить их непосредственно из грида, например вот так:\n1 2 3 4 # Тут min и max значения я дополнительно округлил до сотен hmin = np.round(time_grid_xr.min(), -2) hmax = np.round(time_grid_xr.max(), -2) levels = np.arange(hmax, hmin, -10) Окай, изолинии и векторную заливку мы отрисовали. И тут может возникнуть закономерный вопрос, так а если Matplotlib уже умеет извлекать изолинии из 2D массива данных, то не можем ли мы извлечь отрисованное из графика Matplotlib для собственных нужд? Можем. Именно для этого мы предусмотрительно сохранили «слои» с изолиниями и контурной заливкой в переменные isoline и collec_poly. А теперь давайте посмотрим, что там внутри?\nLineСтрингиString. Разбираем карту на изолинии\rНа первый взгляд, библиотека Matplotlib имеет довольно сложную структуру, а один вид ее документации может заставить вас рыдать в подушку, как девчонку. Читать документацию от корки до корки мы с вами конечно же не будем. Вместо этого попробуем применить грубую мужскую силу и решить нашу задачу методом реверс инжиниринга научного «тыка», попутно обращаясь к документации библиотеки (только в случае острой необходимости).\nВызовем переменную isoline и посмотрим, что нам вернет интерпретатор Python:\n\u0026lt;matplotlib.contour.QuadContourSet at 0x1e40f8f2888\u0026gt;\nНичего не понятно, но очень интересно\u0026hellip; Копируем данную строку в поисковик, чтобы получить больше информации. Получаем ответ: matplotlib.contour.QuadContourSet — это класс в Matplotlib, который представляет собой набор контурных линий или заполненных областей, сгенерированных функциями contour() или contourf(), а именно их мы использовали для создания контурных графиков и изолиний из 2D-данных.\nИными словами, как только мы вызываем функцию contour() или contourf(), она возвращает нам экземпляр класса QuadContourSet, который инкапсулирует (что?! О_о) всю информацию о сгенерированных контурах. Если еще проще: QuadContourSet — это грёбаная коробка, в которой хранится информация о контурных графиках, из которой в любой момент можно достать необходимое для дальнейших манипуляций. Именно то, что нам нужно.\nСправедливости ради нужно отметить, что коробка эта имеет дополнительные отделения — атрибуты, в которых хранится разная информация о наших контурных графиках. С атрибутами класса QuadContourSet можно ознакомиться здесь. Нам пригодятся эти:\nсollections: список объектов LineCollection (для contour()) или PolyCollection (для contourf()), которые представляют собой фактические графические элементы контуров levels: список числовых значений уровней контуров\\изолиний allsegs: список, содержащий координаты сегментов контура для каждого уровня Теперь, когда у нас есть больше информации, давайте попробуем вытащить что-нибудь из этого пакета с пакетами.\n1 isoline.collections Данный фрагмент когда вернет нам следующее:\n\u0026lt;a list of 20 PathCollection objects\u0026gt;\nPython вернул нам список из двадцати PathCollection. Прям кроличья нора какая-то\u0026hellip; Но нас это не остановит! Давайте двигаться последовательно: это список, а значит мы может получить конкретный его элемент по индексу. Давайте извлечем нулевой элемента списка:\n1 isoline.collections[0] Получаем:\n\u0026lt;matplotlib.collections.PathCollection at 0x1e40f90a848\u0026gt;\nОсталось выяснить что такое PathCollection. Идем в документацию, читаем. PathCollection — это набор (коллекция) линейных объектов Path. Да ладно! Они могут быть линейными, криволинейными, замкнутыми, разъединенными. Базовое хранилище для этих объектов включает в себя два numpy массива данных:\nvertices: в нем хранятся координаты вершин линейных объектов (в нашем случае изолиний) codes: numpy массив, который содержит в себе вспомогательную информацию, которая используется Matplotlib для отрисовки линейных объектов, в зависимости от их типа. Что характерно, оба массива имеют одинаковую размерность. Последнее с чем осталось разобраться, прежде чем мы наконец-то перейдем к коду: слово Collection в названии PathCollection как бы намекает нам, что внутри него не один Path. You know? Дело в том, что изолинии внутри Collections группируются по значению изолиний — levels. Проверим:\n1 isoline.levels 1 2 3 array([-1890., -1880., -1870., -1860., -1850., -1840., -1830., -1820., -1810., -1800., -1790., -1780., -1770., -1760., -1750., -1740., -1730., -1720., -1710., -1700.]) Итого: 20 PathCollection (ровно по количеству уровней наших изолиний), каждая из которых может содержать несколько изолиний (Path) одного уровня. Значит для того, чтобы собрать все это дерьмо в одну кучу нам потребуется запихнуть цикл в цикл. По итогу: один цикл будем проходиться по каждой из 20 PathCollection, а вложенный цикл будет собирать информацию о каждом Path. Фух!\nКстати, получить те самые два numpy массива vertices и codes из PathCollection можно с помощью функции get_paths(). Изолинии будем хранить в GeoDataFrame в виде геометрического примитива линии LineString. Теперь точно все. Погнали!\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from shapely.geometry import LineString line_coll = [] #Cюда будем сохранять координаты вершин изолиний label_coll = [] #Cюда будем сохранять подписи изолиний # Последовательно проходим по всем PathCollection for i, collection in enumerate(isoline.collections): # Запоминаем значение изолинии для текущей PathCollection label_isoline = isoline.levels[i] # На каждом шаге просматриваем все изолинии из PathCollection одного уровня for path in collection.get_paths(): # Возвращает два массива vertices и codes line_coll.append(path.vertices) # Берем только vertices и сохраняем в список label_coll.append(label_isoline) # Сохраняем подпись текущей изолинии в список # Перобразуем список списков координат в геометрию LineString isoline_geom = [LineString(line) for line in line_coll] # Создаем линейный GeoDataFrame с изолиниями isoline_gdf = gpd.GeoDataFrame({\u0026#39;isoline\u0026#39;: label_coll}, crs = \u0026#39;EPSG:32640\u0026#39;, geometry = isoline_geom) isoline_gdf [!attention] Упоминание документации о недопустимости обращения к вершинам напрямую. Но когда настоящих панков останавливали правила? Я, честно говоря, не вижу веских причин для того, чтобы придерживаться этого совета. Просто будем держать эту информацию в уме, а когда придет время воспользоваться этими знаниями, всенепременно так и поступим.\nНу вот в общем-то и все. Можно сохранять в shp-файл. Хотя\u0026hellip; Вы же не думаете, что мы остановимся на этом? Это было бы слишком просто для нас. На кой черт нам эти бездушные изолинии?! Давайте немного усложним задачу. Было бы неплохо, если бы наш скрипт дополнительно искал замкнутые контуры изолиний и сохранял их в отдельный полигональный GeoDataFrame, да еще и площадь этих структур считал.\nС чего начнем? Для начала нужно понять, как мы будем определять замкнута ли изолиния или нет. Один из вариантов — использовать информацию из массива codes. Тем более, что сам Matplotlib использует его для отрисовки линий. Условно, по номеру кода в массиве codes Matplotlib понимает как соединять между собой две соседние точки из массива вершин vertices. Если пораскинуть мозгами, то не сложно догадаться, что нас интересует код последней точки массива codes у каждого Path. И тут все просто: если массив codes заканчивается цифрой ====79====, то линия считается замкнутой. Почему именно ====79====, спросите вы меня? Йух знает! Так написано в документации Matplotlib. Подробности можно найти тут.\nПо идее, все что нужно сделать — это добавить условие во вложенный цикл for с проверкой последнего значения в массиве codes для каждого линейного объекта Path. Если последнее значение равно ====79==== — считаем, что контур замкнут, в противном случае нет. Результат проверки пока будем сохранять в отдельный столбец нашего GeoDataFrame с изолиниями.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 line_coll = [] # Cюда будем сохранять координаты вершин изолиний label_coll = [] # Cюда будем сохранять подписи изолиний type_coll = [] # Сюда сохраним тип полигона (открытый\\закрытый) # Последовательно проходим по всем PathCollection for i, collection in enumerate(isoline.collections): # Запоминаем значение изолинии для текущей PathCollection label_isoline = isoline.levels[i] # На каждом шаге просматриваем все изолинии из PathCollection одного уровня for path in collection.get_paths(): # Возвращает два массива vertices и codes line_coll.append(path.vertices) # Берем только vertices и сохраняем в список label_coll.append(label_isoline) # Сохраняем подпись текущей изолинии в список # Проверяем замкнут ли полигон if path.codes[-1] == 79: # Проверяем последнее значение массива codes type_coll.append(\u0026#39;closed\u0026#39;) else: type_coll.append(\u0026#39;opened\u0026#39;) # Перобразуем список списков координат в геометрию LineString isoline_geom = [LineString(line) for line in line_coll] # Создаем линейный GeoDataFrame с изолиниями isoline_gdf = gpd.GeoDataFrame({\u0026#39;isoline\u0026#39;: label_coll, \u0026#39;type\u0026#39;: type_coll}, crs = \u0026#39;EPSG:32640\u0026#39;, geometry = isoline_geom) isoline_gdf Посмотрим что получилось. Визуализируем наши данные, раскрасив изолинии в соответствии со значением из поля type:\n1 2 3 fig, ax = plt.subplots() isoline_gdf.plot(column=\u0026#39;type\u0026#39;, legend=True, ax=ax) ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) Так, стоп. А это чей валет из колоды выпал?\nИ он там не один такой, если присмотреться внимательно. Первое, что приходит в голову: наверняка в этом контуре есть разрыв. В ходе внимательного визуального анализа контура — разрывов обнаружено не было. Кстати, для того, чтобы запустить график в интерактивном режиме (с возможностью увеличения необходимых областей и т.д.) необходимо использовать магическую команду %matplotlib notebook. Просто добавьте ее в ячейку с кодом Jupyter Notebook. Вернемся к нашей проблеме: давайте посмотрим, что содержится в массивах vertices и codes замкнутого контура, тип которого определяется как «открытый». Я взял маленький контур из 5 точек. В нашем массиве GeoDataFrame он значится под индексом 39:\n1 isoline_gdf.geometry[39] В структуре Matplotlib он находится в коллекции (PathCollection) с индексом 4 (изолинии со значением отметок -1850 м.), полигон с индексом 13 (Path):\n1 isoline.collections[4].get_paths()[13] 1 2 3 4 5 Path(array([[ 130977.7593 , 1123344.85384041], [ 130970.24691695, 1123339.3653 ], [ 130977.7593 , 1123334.67135227], [ 130984.14078668, 1123339.3653 ], [ 130977.7593 , 1123344.85384041]]), array([1, 2, 2, 2, 2], dtype=uint8)) Видно, что массив codes заканчивается кодовым значением 2, и по логике Matplotlib не является замкнутой линией. При этом координаты первой и последней точек (массив vertices) совпадают вплоть до последнего знака, что говорит о том, что данная линия является замкнутой. То есть она замкнута и не замкнута одновременно\u0026hellip; Прям линия Шредингера какая-то.\nОк, пройдемся по всему пройденному «маршруту» координат данной линии. Посмотрим в промежуточном списке, куда мы сохраняли наши Path из цикла (переменная line_coll):\n1 line_coll[39] 1 2 3 4 5 array([[ 130977.7593 , 1123344.85384041], [ 130970.24691695, 1123339.3653 ], [ 130977.7593 , 1123334.67135227], [ 130984.14078668, 1123339.3653 ], [ 130977.7593 , 1123344.85384041]]) Все ок. Осталось посмотреть координаты этой записи в объекте GeoDataFrame:\n1 isoline_gdf.iloc[39].geometry.xy 1 2 (array(\u0026#39;d\u0026#39;, [130977.7593, 130970.24691695103, 130977.7593, 130984.14078667603, 130977.7593]), array(\u0026#39;d\u0026#39;, [1123344.85384041, 1123339.3653, 1123334.6713522696, 1123339.3653, 1123344.8538404102])) Какого черта?! В каждой второй координате X и Y появилось три лишних знака после запятой (было 8, а стало 11)! И ТЕПЕРЬ уже координаты X первой и последней точек совпадают, а координаты Y этих же точек ОТЛИЧАЮТСЯ, причем именно в последних двух знаках после запятой. Тех самых, которые появляются как по мановению волшебной палочки, ёпта. Да и вообще, мне одному кажется странным, что во всех массивах координат отличается количество знаков после запятой через одну вершину?!!\nГрёбаный Path этого Matplotlib!!!! И как с этим жить? Как по мне, так тут два варианта: либо мы с вами поймали bug-а за яйца, либо где-то жидко обосрались испражнились под себя и нужно было все-таки читать документацию более внимательно. В любом случае, отступать мы не собираемся! Предлагаю просто переписать часть кода с условием: теперь вместо того, чтобы проверять последнее значение из массива codes мы будем сравнивать координаты первой и последней точек из массива vertices. Если координаты будут совпадать — будем считать, что линия является замкнутой. А для пущей надежности немного округлим все значения координат, скажем до третьего знака после запятой.\nPolygon. Определяем тип замкнутого контура\rДля удобства, замкнутые изолинии будем собирать в отдельный полигональный GeoDataFrame. Для описания геометрии замкнутых полигональных объектов в GeoPandas используется примитив Polygon. Объекты Polygon создаются по аналогии с LineString, так что ничего принципиально нового тут не будет. Пока.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from shapely.geometry import LineString, Polygon line_coll = [] # Cюда будем сохранять координаты вершин изолиний label_coll = [] # Cюда будем сохранять подписи изолиний type_coll = [] # Сюда сохраним тип полигона (открытый\\закрытый) poly_coll = [] # Cюда будем сохранять координаты замкнутых изолиний poly_label = [] # Cюда будем сохранять подписи замкнутых изолиний # Последовательно проходим по всем PathCollection for i, collection in enumerate(isoline.collections): # Запоминаем значение изолинии для текущей PathCollection label_isoline = isoline.levels[i] # На каждом шаге просматриваем все изолинии из PathCollection одного уровня for path in collection.get_paths(): # Возвращает два массива vertices и codes # Сохраним координаты первой и последней точек, предварительно округлив first_x, first_y = np.round(path.vertices[0], 3) last_x, last_y = np.round(path.vertices[-1], 3) line_coll.append(path.vertices) # Берем только vertices и сохраняем в список label_coll.append(label_isoline) # Сохраняем подпись текущей изолинии в список # Наше новое условие проверки линии на предмет замкнутости if first_x == last_x and first_y == last_y: type_coll.append(\u0026#39;closed\u0026#39;) poly_coll.append(path.vertices) poly_label.append(label_isoline) else: type_coll.append(\u0026#39;opened\u0026#39;) # Перобразуем список списков координат в геометрию LineString isoline_geom = [LineString(line) for line in line_coll] # Перобразуем список координат замкнутых контуров в геометрию Polygon struc_geom = [Polygon(poly) for poly in poly_coll] # Создаем линейный GeoDataFrame с изолиниями isoline_gdf = gpd.GeoDataFrame({\u0026#39;isoline\u0026#39;: label_coll, \u0026#39;type\u0026#39;: type_coll}, crs = \u0026#39;EPSG:32640\u0026#39;, geometry = isoline_geom) # Создаем полигональный GeoDataFrame с замкнутыми изолиниями closed_gdf = gpd.GeoDataFrame({\u0026#39;isoline\u0026#39;: poly_label}, crs = \u0026#39;EPSG:32640\u0026#39;, geometry=struc_geom) # Визуализируем GeoDataFrame fig, ax = plt.subplots() isoline_gdf.plot(column=\u0026#39;type\u0026#39;, legend=True, ax=ax) closed_gdf.plot(ax=ax, alpha=0.3) ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) Ну вот, совсем другое дело! Все валеты в колоде. И теперь у нас есть отдельный GeoDataFrame с замкнутыми изолиниями — closed_gdf. Осталось научиться отличать положительные замкнутые (антиклинальные) структуры от отрицательных. Самая простая идея, которая пришла мне в голову — вычислить центральную точку (центроид) для каждого полигона и сравнивать значение отметок в этой точке со значением самого контура. Давайте по шагам:\nВычисляем центроид для каждого полигона Снимаем значение абсолютной отметки с карты в точке центроида (этому мы научились в прошлой заметке) Сравниваем значение изолинии со значением абсолютной отметки центроида: если значение абсолютной отметки центроида больше, чем значение изолинии замкнутого контура, то структура считается положительной (антиклинальной). В противном случае — отрицательной. Для вычисления центроида по геометрии объекта в GeoPandas есть стандартная функция centroid. Но есть нюанс: использование centroid не гарантирует нам, что точка попадет во внутреннюю область нашего полигона. Представьте замкнутый контур в виде подковы: вычисление centroid для такого полигона приведет к тому, что его центральная точка окажется вне контура полигона. Для того, чтобы центральная точка гарантировано оказалась внутри контура полигона, в GeoPandas есть альтернативная функция representative_point().\nДля вычисления площади полигонов в GeoPandas также есть специальная функция — area. Вот так просто. В этом и заключается все преимущество пространственного типа данных. Теперь мы знаем все необходимое, чтобы закончить наш скрипт:\n1 2 3 4 5 6 7 8 9 10 11 12 13 closed_gdf[\u0026#39;area\u0026#39;] = closed_gdf.area poly_centroid = closed_gdf.representative_point() x_centroid = xr.DataArray(poly_centroid.x, dims=\u0026#39;points\u0026#39;) y_centroid = xr.DataArray(poly_centroid.y, dims=\u0026#39;points\u0026#39;) depth_centroid = time_grid_xr.sel(x = x_centroid, y = y_centroid, method = \u0026#39;nearest\u0026#39;) closed_gdf[\u0026#39;h_centroid\u0026#39;] = depth_centroid closed_gdf.loc[closed_gdf[\u0026#39;h_centroid\u0026#39;] \u0026gt; closed_gdf[\u0026#39;isoline\u0026#39;], \u0026#39;type\u0026#39;] = \u0026#39;positive\u0026#39; closed_gdf.loc[closed_gdf[\u0026#39;h_centroid\u0026#39;] \u0026lt; closed_gdf[\u0026#39;isoline\u0026#39;], \u0026#39;type\u0026#39;] = \u0026#39;negative\u0026#39; closed_gdf Клац-клац и вот у нас: краткая сводка по структурам, необходимые shp-файлы для оформления графики в ГИС-проекте и несколько часов сэкономленного времени.\nСохраним shp-файлы\n1 2 3 4 crs = \u0026#39;EPSG:32640\u0026#39; isoline_gdf.to_file(driver=\u0026#39;ESRI Shapefile\u0026#39;, filename=r\u0026#39;isoline.shp\u0026#39;, crs = crs) closed_gdf.to_file(driver=\u0026#39;ESRI Shapefile\u0026#39;, filename=r\u0026#39;structure.shp\u0026#39;, crs = crs) MultiPolygon. Векторная заливка карты\rА что насчет цветной заливки? Да все аналогично! Без лишних слов напишем функцию, которая на вход будет принимать контурный график Matplotlib, а на выходе выплёвывать полигональный shp-файл с векторной заливкой нашей карты:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def collec_to_gdf(collec_poly, crs = \u0026#39;EPSG:32640\u0026#39;): polygons, color = [], [] for i, polygon in enumerate(collec_poly.collections): mpoly = [] for path in polygon.get_paths(): try: path.should_simplify = False poly = path.to_polygons() # Полигон содержит внешний контур и возможные внутренние отверстия: exterior, holes = [], [] if len(poly) \u0026gt; 0 and len(poly[0]) \u0026gt; 3: # Первым в списке идет внешний контур полигона exterior = poly[0] # Все остальные - отверстия if len(poly) \u0026gt; 1: holes = [h for h in poly[1:] if len(h) \u0026gt; 3] mpoly.append(Polygon(exterior, holes)) except: print(\u0026#39;Warning: Ошибка геометрии. Полигон: #{}\u0026#39; .format(i)) if len(mpoly) \u0026gt; 1: mpoly = MultiPolygon(mpoly) polygons.append(mpoly) color.append(clr.rgb2hex(polygon.get_facecolor().tolist()[0])) elif len(mpoly) == 1: polygons.append(mpoly[0]) color.append(clr.rgb2hex(polygon.get_facecolor().tolist()[0])) return gpd.GeoDataFrame(data = {\u0026#39;Color\u0026#39;: color, \u0026#39;Isoline\u0026#39;: collec_poly.get_array().data}, geometry = polygons, crs = crs) В отдельное поле мы сохранили цвет каждой области в hex формате, использовав функцию get_facecolor(). А геометрию полигонов, в этот раз, хранили в сгруппированном виде. В GeoPandas для этого есть специальный тип геометрии MultiPolygon. Аналогично работает MultiLineString для линейных объектов. Итак, посмотрим, что получили:\n1 2 3 4 5 poly_gdf = collec_to_gdf(collec_poly) fig, ax = plt.subplots() poly_gdf.plot(ax=ax, color=poly_gdf[\u0026#39;Color\u0026#39;]) ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) ","date":"2025-07-13T00:00:00Z","image":"http://localhost:1313/p/%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%B0%D1%8F-%D0%B3%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0-%D0%B2-python/vector-graphic-in-python_hu_8169da7bf3cdb1e.png","permalink":"http://localhost:1313/p/%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%B0%D1%8F-%D0%B3%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0-%D0%B2-python/","title":"Векторная ГеоГрафика в Python"},{"content":"\r«Но я лентяй! Я ненавижу работать! Особенно я ненавижу тяжёлый труд во всех его проявлениях! Хитрые и изящные решения — вот мой конёк!»\n― Элиезер Юдковский, «Гарри Поттер и методы рационального мышления» «Торжественно клянусь, что замышляю шалость, и только шалость!»\nЧто, если я скажу вам, что программирование для современного ученого и инженера — это не просто киллер-фича в резюме, а спасательный трос в мире больших данных, рутинных процессов и нестандартных задач. Именно этой идеи придерживается наше сообщество, одним из ключевых принципов которого является принцип «DIY» (Do it yourself — сделай сам). Идея этого принципа проста: в решении своих повседневных задач мы полагаемся не на монструозные коммерческие костыли в красивой обертке готовые решения, а оптимизируем свои решения с помощью копролитов и палок Python и данных.\nКаждый день, от проекта к проекту, мы сталкивается с лавиной данных, которые зачастую невозможно быстро и качественно обработать стандартными инструментами. Готовые программные пакеты хороши для типовых задач, но они бессильны, когда нужно решить что-то нестандартное — будь то автоматическая обработка сотен скважин или поиск скрытых закономерностей в данных. Программирование преодолевает эти ограничения: оно превращает рутину в несколько строк кода, а нестандартные задачи — в вызов, который вы в силах принять.\nЦель данной заметки не научить вас программированию, нет. Для этого существует множество отличных источников. Наша цель — дать вам пинок под зад краткое руководство к действию. На примере реальных данных показать, что программирование на Python это максимально просто. Просто настолько, что после прочтения этой заметки даже ваша бабушка пересядет со спиц в Jupyter Notebook.\nЕсли после этого текста у тебя не зачесались руки написать хотя бы строчку кода — проверь пульс. Ты точно ещё жив? Если пульс есть, но желания так и не появилось — сгоняй нам за пивком, а мы пока начинаем.\nОт таблиц к пространственным данным\rПрежде чем мы погрузимся в увлекательный мир программирования, хотелось бы отметить, что при подготовке данной заметки, были использованы геолого-геофизические данные, распространяемые с курсом практических упражнений по сейсмической интерпретации под авторством А.С. Кирилова и К.Е. Закревского. Со своей стороны, SciPunk выражает респект авторам курса за их работу и приверженность принципам свободного распространения обучающих материалов. С деятельностью авторов курса можно ознакомиться на их сайте или сообществе Вконтакте. А теперь стартуем!\nПожалуй одной из самых распространенных форм представления геолого-геофизической информации являются таблицы. С ними мы сталкиваемся постоянно и повсеместно. Таблицы могут храниться как в текстовом (ASCII), так и в бинарном формате.\nНачнем с простого: импортируем таблицу со стратиграфическими отбивками из текстового файла формата CSV. Для работы с таблицами в Python есть замечательная библиотека Pandas. Для того, чтобы ей воспользоваться, необходимо для начала ее импортировать:\n1 import pandas as pd # Импортируем библиотеку Pandas Далее, прочитаем содержимое файла TOPs.dat с использованием функции read_csv() из библиотеки Pandas. Для этого нам потребуется всего одна строчка кода. Все что нужно сделать — передать в качестве аргументов в функцию read_csv() несколько параметров:\nПуть до нашего файла 'data\\wells\\TOPs.dat' Символ, использующийся в качестве разделителя в фале csv. В нашем случае это символ пробела sep = ' ' Дополнительно используем параметр skipinitialspace = True для игнорирования лишних пробелов 1 2 3 #Чтение данных из файла CSV well_tops = pd.read_csv(\u0026#39;data\\wells\\TOPs.dat\u0026#39;, sep = \u0026#39; \u0026#39;, skipinitialspace = True) well_tops Вуаля! Теперь в переменной well_tops хранится объект DataFrame с нашей таблицей стратиграфических отбивок.\nNot Bad! С этим уже можно работать, но мы пойдем дальше. Отличительной особенностью геолого-геофизической информации является ее пространственная привязка. Для тех, кто забыл что это такое, ниже приведена база небольшая информационная справка.\n[!info] Геопространственные данные Данные, которые связывают некоторую геологическую информацию с конкретным географическим положением, называются геопространственными, и являются основой геоинформационных систем.\nПоложение данных в пространстве, может быть представлено двумя системами координат: географической и картографической (прямоугольной). Географическая система координат характеризуется значениями широты и долготы. Такие координаты являются неспроецированными и указывают на конкретную точку поверхности Земли. Для того, чтобы отобразить трехмерную Земную поверхность на двумерную плоскость применяются математические преобразования, называемые картографическими проекциями.\nКартографическая система координат представляет собой спроецированные на плоскость географические координаты широты и долготы, и указывает на точку двумерной (картезианской) плоскости изображающую Земную поверхность, c координатами X и Y.\nДля работы с картографической системой координат важно знать, какая проекция использовалась для отображения пространственных объектов на плоскость. Существует множество различных проекций, каждая из которых подходит под определенные участки Земной поверхности, но ни одна из них не может точно описать всю поверхность, не внося некоторых искажений.\nГеографическая система координат хороша тем, что указывает конкретное положение на Земной поверхности, но усложняет геопространственные вычисления. Картографическая система координат, наоборот, значительно упрощает геопространственные вычисления, но обладает вышеизложенными недостатками, связанными с невозможностью точного описания всей Земной поверхности одной проекцией.\n— Дед, ты бредишь? В таблице же есть столбцы с координатами! — скажете вы мне. — Yep — отвечу вам я.\nТо, что кажется очевидным для вас, может быть совершенно неочевидным для вашего компутера. Действительно, в нашей таблице присутствуют столбцы с прямоугольными координатами X и Y, но в ней не содержится дополнительной информации о системе координат и проекции.\nДля описания геометрии пространственных типов данных в геоинформатике используются геометрические примитивы: точка Point, отрезок LineString, многоугольник Polygon. Примитивы представляют собой набор координат, характеризующих положение этих данных в одной из систем координат. В нашем случае стратиграфические отбивки представляют собой точечные данные, для описания которых подходит геометрический примитив Point.\nТеперь давайте модифицируем нашу таблицу так, чтобы она отвечала всем требованиям пространственного типа данных. С этим нам поможет библиотека GeoPandas. По сути это те же яйца, только в профиль тот же Pandas, только с поддержкой всех особенностей пространственных данных. Потребуется еще две строчки кода (хотя можно было бы уложиться и в одну), не считая импорта библиотеки.\n1 2 3 4 5 6 import geopandas as gpd #Конвертируем информацию в пространственный тип данных GeoDataFrame (POINT) geometry = gpd.points_from_xy(well_tops.X, well_tops.Y) gdf_tops = gpd.GeoDataFrame(well_tops, crs = \u0026#39;EPSG:32640\u0026#39;, geometry = geometry) gdf_tops Все что нужно было сделать — это создать специальный объект с описанием геометрии наших данных с помощью функции points_from_xy(), на вход которой мы подали два столбца с координатами well_tops.X и well_tops.Y из нашей исходной таблицы. Затем, на основе нашего DataFrame из переменной well_tops мы создали WunderGeoDataFrame, передав дополнительно переменную с геометрией geometry и информацию о системе координат и проекции crs = 'EPSG:32640' через ее уникальный идентификационный код EPSG. Смотрим что получили:\nНичего не напоминает по структуре? Правильно, точечный shp-файл. Теперь столбцы Well, Surface, X, Y, Z — это атрибутивная информация, а geometry — столбец, который хранит геометрию наших точек aka отбивок. А чтобы окончательно вас в этом убедить, давайте сохраним полученный GeoDataFrame в наш первый самопальный shp-файл, вот так:\n1 2 # Сохраняем отбивки в формате shp-файла gdf_tops.to_file(driver=\u0026#39;ESRI Shapefile\u0026#39;, filename=r\u0026#39;well_tops.shp\u0026#39;, crs=\u0026#39;EPSG:32640\u0026#39;) Как вы уже могли догадаться, читать shp-файлы с помощью библиотеки GeoPandas не сильно сложнее. Просто передаем путь и имя нашего файла в функцию read_file():\n1 2 # Читаем наш новоиспеченный shp-файл gdf_tops = gpd.read_file(\u0026#39;well_tops.shp\u0026#39;) Теперь посмотрим на данные в таблице. Предположим, нам нужно получить список уникальных значений столбца Surface — легко:\n1 gdf_tops[\u0026#39;Surface\u0026#39;].unique() А теперь мы хотим визуализировать отбивки артинского яруса P1ar по всем скважинам. Для начала отфильтруем данные по нашему запросу, а результат сохраним в переменную p1ar_tops:\n1 p1ar_tops = gdf_tops[gdf_tops[\u0026#39;Surface\u0026#39;] == \u0026#39;P1ar\u0026#39;].copy() Затем, отобразим карту с отбивками используя встроенный метод plot(). При этом маркер отбивки покрасим цветом, согласно значению ее абсолютной отметки column = 'Z':\n1 2 3 4 5 import matplotlib.pyplot as plt fig, ax = plt.subplots() p1ar_tops.plot(column = \u0026#39;Z\u0026#39;, legend = True, cmap = \u0026#39;Spectral_r\u0026#39;, ax = ax) ax.grid(True, linestyle = \u0026#39;dotted\u0026#39;) Ваша первая карта, поздравляю! Да, выглядит не очень впечатляюще, но мы только в начале пути, наберитесь терпения. Оставим пока наши отбивки и попробуем подгрузить что-нибудь еще.\nДвумерные данные. Поверхности (гриды)\rНе одними лишь таблицами живет геолог(иня)\\геофизик(чка). Не менее популярный формат представления геолого-геофизической информации — двумерные поверхности (гриды): структурные карты, карты толщин, карты изохрон, матрицы геофизических полей (гравика, магнитка и пр.) и так далее. Их тоже хотелось бы читать из файла. Ну так давайте попробуем. Возьмем один из распространенных открытых форматов Landmark Z-Map Grid. Здесь тоже есть готовые решения, например библиотека Zmapio. Идем по ссылке, читаем документацию, повторяем:\n1 2 from zmapio import ZMAPGrid time_grid = ZMAPGrid(r\u0026#39;data\\surfaces\\P1ar_time.dat\u0026#39;) Чтобы нашим отбивкам не было одиноко, подгрузим поверхность изохрон (времен пробега волны) отражающего горизонта P1ar, полученную по данным сейсморазведки МОГТ-3D. Импортировали — теперь посмотрим на нее, используя встроенный метод plot():\n1 2 3 4 5 fig, ax = plt.subplots() time_grid.plot(axes=ax, cmap=\u0026#39;Spectral_r\u0026#39;) ax.axis(\u0026#39;scaled\u0026#39;) ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) plt.colorbar() Отлично! Визуализация это конечно здорово, но нам ведь с этими данными еще науку делать работать придется по мере углубления в материал (хотя, откровенно говоря, правильная визуализация данных — уже 70% успеха). Посмотрим, в каком виде zmapio может вернуть нам нашу поверхность. Идем в документацию, смотрим — есть два стула варианта:\nВариант 1: Можно получить двумерный numpy массив используя метод z_values, вот так: time_grid.z_values Вариант 2: Работать с поверхностью, как с базой данных, сконвертировав ее в Pandas DataFrame, вот так: time_grid.to_dataframe()\nВ первом случае нам придется обращаться к данным через нумерованные индексы, которые каким-то образом придется постоянно соотносить с координатами. Во втором - работать с плоским массивом данных, но с возможностью поименованного (непосредственно по координатам) обращения к данным. Такая вот вилка в глаз Мортона (или аксиома Эскобара, кому как угодно).\nНам оно не надо. Благо братики постряпали удобную библиотеку Xarray, которая лишит нас этой головной боли. Xarray — это сын маминой подруги в мире многомерных массивов Python. Простыми словами, Xarray — многомерный Pandas, который позволяет работать с массивами, используя поименованные индексы. Не переживайте, если пока не понятно, сейчас будем разбираться.\nИтак, для удобства последующей работы создадим двумерный xarray из numpy массива нашей поверхности изохрон, предварительно импортировав необходимую библиотеку:\n1 2 3 4 5 6 7 import xarray as xr time_grid_xr = xr.DataArray(time_grid.z_values.T, name=\u0026#39;time\u0026#39;, coords=[time_grid.y_values[:, 0], time_grid.x_values[0]], dims=[\u0026#39;y\u0026#39;, \u0026#39;x\u0026#39;]) Заглянем внутрь полученного массива xarray, вызвав нашу переменную time_grid_xr:\nЧто имеем? Внутри нашего xarray есть двумерный массив времен пробега волны и два массива с координатами X и Y, используя которые мы можем обращаться к нашим данным. Например, теперь мы легко можем получить значения времен в точках скважин (по их координатам) или снять значения времен вдоль линии сейсмического профиля. Но прежде чем мы это сделаем, давайте отобразим наши данные на одной карте:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 fig, ax = plt.subplots() # Визуализируем цветовую расскраску карты по уровням с шагом 10 мс. time_grid_xr.plot.contourf(ax=ax, cmap=\u0026#39;Spectral_r\u0026#39;, levels = np.arange(-1700, -1900, -10)) # Визуализируем изолии времен с шагом 20 мс. time_grid_xr.plot.contour(colors=\u0026#39;black\u0026#39;, ax=ax, linewidths=0.5, levels = np.arange(-1700, -1900, -20), linestyles=\u0026#39;solid\u0026#39;) # Визуализируем отбивки по горизонту P1ar p1ar_tops.plot(ax=ax, markersize = 25, label = \u0026#39;P1ar tops\u0026#39;, zorder=10) ax.legend() # Добавлям легенду нашей карты ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) # Добавлям координатную сетку Здесь мы применили чуть более подробное оформление нашего графика:\nВозвращаемся к нашим индексам. Если бы мы работали с поверхностью как с массивом numpy, мы должны были бы помнить, что \u0026ldquo;координаты\u0026rdquo; массива numpy (индексы) всегда начинаются со значения 0 и идут с шагом 1. Предположим: мы хотим получить значения времени пробега волны с карты изохрон в точке скважины с координатой X и Y. Для того, чтобы это сделать, мы должны были бы сначала определить порядковый номер (индекс) этой точки в системе координат numpy массива, и только по этому индексу получить значение с карты. Сложно, согласитесь? Вместо этого мы предусмотрительно сконвертировали наш numpy массив в формат xarray и теперь можем обращаться к данным напрямую по значению координат, а вся головная боль будет происходить под капотом библиотеки xarray. В конце концов мы с вами геологи\\геофизики, а не пограмисты за 300кк в наносек — нам и своих проблем хватает. Итак, давайте таки получим значения времен в точках скважин с карты изохрон и запишем их в новую колонку times таблицы с отбивками по артинскому ярусу p1ar_tops:\n1 2 3 4 5 6 7 8 # Подготовим два массива с координатами скважин в формате xarray x_points = xr.DataArray(p1ar_tops.X.values, dims=\u0026#39;points\u0026#39;) y_points = xr.DataArray(p1ar_tops.Y.values, dims=\u0026#39;points\u0026#39;) # Формируем запрос для получения значений времен из массива xarray по координатам time_points = time_grid_xr.sel(x = x_points, y = y_points, method=\u0026#39;nearest\u0026#39;) # Записываем полученные значения времен в новый столбец таблицы p1ar_tops p1ar_tops[\u0026#39;times\u0026#39;] = time_points.values Смотрим что получили:\nАтлична-атлична! А теперь построим график зависимости \u0026ldquo;время-глубина\u0026rdquo; (диаграмма рассеяния aka кросс-плот) в точках скважин, например. Полученные уравнения регрессии братья сейсмики иногда используют для пересчета своих карт времен (изохрон) в карты глубин (структурные карты).\n1 2 3 4 5 6 import seaborn as sns sns.lmplot(x=\u0026#39;times\u0026#39;, y=\u0026#39;Z\u0026#39;, data=p1ar_tops) plt.grid(True, linestyle=\u0026#39;dotted\u0026#39;) plt.title(\u0026#39;Time vs. Depth (P1ar)\u0026#39;) plt.show() В этот раз для визуализации графика используем библиотеку Seabron. Это библиотека визуализации красивых статистических графиков, основанная на библиотеке matplotlib.\nКрасивое\u0026hellip; Ну что ж, легкой поступью мы с вами подошли к теме анализа данных. Но не будем забегать вперед! Оставим эту тему на следующий урок по DIY. Обязательно научимся оценивать уравнения линейной регрессии, займемся прогнозами (в том числе с помощью машинного обучения) и оценкой неопределенностей. А пока продолжаем разбираться со входными данными.\nДанные сейсморазведки. Сейсмические разрезы формата SEG-Y\rКарту изохрон мы разобрали, теперь остановимся на данных сейсморазведки, по которым эта карта была построена. Начнем с временных сейсмических разрезов МОГТ-2D. Стандартом хранения сейсмических данных является бинарный формат SEG-Y, разработанный международным обществом геофизиков-разведчиков — Society of Exploration Geophysicists (SEG). При желании, со спецификациями данного формата можно ознакомиться на сайте сообщества.\nБлаго об этом уже кто-то позаботился за нас, ознакомился со спецификациями к данному типу файлов и любезно предоставил библиотеку для работы с ними. На самом деле таких библиотек на данный момент существует несколько. ObsPy — одна из таких библиотек. Она ориентирована на обработку сейсмологических данных и предоставляет возможность чтения данных в формате SEG-Y. В данном уроке мы воспользуемся возможностями именно этой библиотеки.\n[!attention] Как известно, временной сейсмический разрез представляет собой набор так называемых трасс (временных рядов), которые получаются путем суммирования сейсмограмм общей глубинной точки (ОГТ). Поскольку библиотека ObsPy ориентирована, в первую очередь, на работу с данными сейсмологии, то с сейсмическими разрезами она работает именно в контексте временных рядов. Это может показаться не совсем удобно для работы с 2D разрезами и совершенно непригодно для данных 3D, но для понимания структуры формата SEG-Y данная библиотека подходит как нельзя лучше. Более совершенные библиотеки для работы с данными сейсморазведки будут разобраны в следующих Dungeon master-классах\nОт слов к делу: прочитаем данные SEG-Y файла (временного сейсмического разреза МОГТ-2D). Используем функцию _read_segy из библиотеки ObsPy:\n1 2 3 4 from obspy.io.segy.segy import _read_segy stream = _read_segy(r\u0026#39;data\\seismic\\22.segy\u0026#39;, headonly=True) # Читаем stream # Смотрим что получили Функция возвращает нам объект, который содержит в себе 958 трасс, о чем нам любезно сообщает интерпретатор Python: 958 traces in the SEG Y structure. К конкретной трассе можно обратиться по ее индексу (начиная с нулевого). Так и поступим, заодно сразу ее визуализируем:\n1 2 3 4 5 6 7 # Выдернем нулевую трассу из 958 one_trace = stream.traces[0] # Крендель в квадратных скобках и есть индекс plt.figure(figsize=(16,1)) plt.plot(one_trace.data) plt.grid(True, linestyle=\u0026#39;dotted\u0026#39;) plt.show() Так и есть - трасса. По оси Y здесь амплитуда сигнала, а вот с осью X есть нюанс - вместо привычного двойного времени пробега волны (TWT) здесь отражены дискреты сигнала. Чтобы это исправить, нам нужно получить больше информации о сигнале, как минимум шаг его дискретизации.\n— А где его взять?!!11одинодин — спросите вы.\nДополнительная информация о сигнале хранится в заголовках SEG-Y файла, их есть несколько:\nТекстовый заголовок файла: stream.textual_file_header. Не обязателен к заполнению, нет четкого стандарта его заполнения, а посему часто бывает пустым или содержит нечто бессмысленное Бинарный заголовок файла: stream.binary_file_header имеет четкую структуру и именно там можно найти то, что нас интересует Кроме того, у каждой трассы есть свой заголовок, доступ к которому можно получить через stream.traces[0].header. Информация о шаге дискретизации тут тоже присутствует Теперь давайте пофиксим нашу трассу с учетом вновь полученной информации:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 import numpy as np # Шаг дискретизации сигнала dt = stream.binary_file_header.sample_interval_in_microseconds / 1e+03 # Количество отчетов в трассе samples = stream.binary_file_header.number_of_samples_per_data_trace twt = np.arange(0, samples * dt, dt) # Массив двойного времени волны (TWT) one_trace = stream.traces[0] plt.figure(figsize=(16,1)) plt.plot(twt, one_trace.data) plt.grid(True, linestyle=\u0026#39;dotted\u0026#39;) plt.show() Теперь все верно. Длина записи сигнала в данном случае составляет 3.6 секунды. Осталось получить разрез целиком. Поскольку ObsPy работает с трассами, то пройдемся по ним в цикле, а результат сохраним в наш любимый xarray:\n1 2 3 4 5 6 7 8 9 10 11 # Собираем разрез из трасс с помощью цикла data = np.stack(t.data for t in stream.traces) seismic_xr = xr.DataArray(data.T, name=\u0026#39;amplitude\u0026#39;, coords=[-twt, np.arange(1, len(stream.traces) + 1)], dims=[\u0026#39;TWT\u0026#39;, \u0026#39;Pickets\u0026#39;]) fig, ax = plt.subplots(figsize=(9,6)) seismic_xr.plot(vmin=-40, vmax=40, cmap=\u0026#39;RdBu_r\u0026#39;, ax=ax) ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) Супер! Осталось разобраться с геометрией профиля. Координаты каждой трассы можно получить через заголовки самих трасс stream.traces[0].header. В нашем случае, координаты были записаны здесь:\nX: stream.traces[0].header.source_coordinate_x Y: stream.traces[0].header.source_coordinate_y\nКстати, никогда не стесняйтесь экспериментировать с кодом: заглядывайте внутрь объектов, смотрите из чего они состоят, обращайтесь к официальной документации и не забывайте яндексить гуглить (лучше всего на английском языке. Так сложилось, что вероятность получить ответ на свой вопрос в англоязычном сегменте интернета гораздо выше).\nС местоположением координат внутри файла разобрались. Но не будем же мы считывать координаты каждого профиля по отдельности? Как-никак мы с вами тут про автоматизацию говорили. Говорили? Ну было и было\u0026hellip;\nЯ фантазирую так: как будто мы написали скрипт, который шарится внутри заданной папки в поисках SEG-Y файлов, попутно собирая из них необходимую информацию (включая геометрию профилей), а на выходе выдает нам GeoDataFrame со схемой изученности данными сейсморазведки.\nОх и напифантазировал я на свою голову\u0026hellip; Придется отвечать за базар. Ок, дайте мне три дня. Чтобы было проще, разобьем нашу фантазию на подфантазии:\nНаписать функцию, которая будет осуществлять поиск файлов с заданным расширением файлов внутри заданной папки, а на выходе выдавать нам DataFrame с находками Написать функцию, которая будет последовательно проходить по списку найденных SEG-Y файлов из DataFrame, считывать из них необходимую информацию, а на выходе формировать GeoDataFrame с геометрией сейсмических профилей Звучит неплохо. Давайте начнем с первого пункта:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import os def scan_files(path, type_files = (\u0026#39;.sgy\u0026#39;, \u0026#39;.segy\u0026#39;)): path_files = [] # Сюда будем сохранять пути файлов name_files = [] # Cюда имена файлов # Рекурсивный поиск файлов в заданной директории for foldername, subfolder, filename in os.walk(path): # Отфильтруем лишние файлы sgy = filter(lambda x: x.endswith(type_files), filename) for filename in sgy: # Пройдемся циклом по оставшимся файлам # Формируем полный путь до текущего файла current_file = foldername + \u0026#39;\\\\\u0026#39; + filename path_files.append(current_file) # Сохраняем путь в список name_files.append(filename) # Сохраняем имя файла в список # Формируем DataFrame из списков и возращаем из функции return pd.DataFrame({\u0026#39;File\u0026#39;: name_files, \u0026#39;Path\u0026#39;: path_files}) Итак, мы написали функцию scan_files, которая реализует функционал из первой подзадачи (см. выше). Функция ожидает на вход два аргумента:\nПуть в формате строки, где необходимо искать файлы — path Тип файлов в формате кортежа Pyhton, которые мы собираемся искать Функция осуществляет рекурсивный поиск файлов заданного типа (включая вложенные каталоги) и возвращает DataFrame с найденными файлами. Проверим: вызовем нашу функцию, передав необходимые аргументы, а результат сохраним в переменную sgy_files\n1 2 sgy_files = scan_files(r\u0026#39;data\\seismic\u0026#39;, type_files = (\u0026#39;.sgy\u0026#39;, \u0026#39;.segy\u0026#39;)) sgy_files Отлично! Теперь мы можем использовать цикл для перебора содержимого полученного DataFrame, и считать необходимую информацию из найденных файлов SEG-Y:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 geometry = [] # Список для координат профилей file = [] # Список для имен файлов correct_sgy = [] # Список полного пути до файлов incorrect_sgy = [] # Список кривых файлов, которые не открылись # Пройдемся циклом по строкам DataFrame найденных файлов for row in sgy_files.iterrows(): try: stream = _read_segy(row[1][\u0026#39;Path\u0026#39;], headonly=True) except: print (\u0026#39;Ошибка чтения файла: \u0026#39; + str(row[1][\u0026#39;Path\u0026#39;])) incorrect_sgy.append(row[1][\u0026#39;Path\u0026#39;]) else: x_coord = [t.header.source_coordinate_x for t in stream.traces] y_coord = [t.header.source_coordinate_y for t in stream.traces] geometry_list = list(zip(x_coord, y_coord)) geometry.append(geometry_list) correct_sgy.append(row[1][\u0026#39;Path\u0026#39;]) file.append(row[1][\u0026#39;File\u0026#39;]) print (\u0026#39;Файл обработан: \u0026#39; + str(row[1][\u0026#39;Path\u0026#39;])) geometry = [LineString(line) for line in geometry] seismic_gdf = gpd.GeoDataFrame({\u0026#39;File\u0026#39;: file, \u0026#39;Path\u0026#39;: correct_sgy}, crs=\u0026#39;EPSG:32640\u0026#39;, geometry=geometry) seismic_gdf FUCK YEAH! Кажется у нас получилось. Конечно, я немного схалтурил и не стал оформлять код в функцию, а еще не вынул дополнительную важную информацию из заголовков, но моя основная задача заключалась в том, чтобы научить вас ловить рыбу, а не накормить ей. Think about it… Зато я добавил исключения try — except, чтобы скрипт не спотыкался на кривых SEG-Yях. В качестве домашнего задания оставляю вам доработку напильником данного скрипта с учетом вышесказанного и ваших смелых идей. Результаты домашки будем рады видеть в нашем telegram-канале, в комментариях под новостью о статье.\nКстати, GeoDataFrame с положением сейсмических профилей seismic_gdf можете сохранить в shp-файл, как мы делали это с отбивками (см. выше) и загрузить его в ваш ArcGIS QGIS проект.\nА теперь предлагаю нанести все наши данные на одну обзорную схему:\n1 2 3 4 5 6 7 8 9 fig, ax = plt.subplots() time_grid_xr.plot.contourf(ax = ax, cmap = \u0026#39;Spectral_r\u0026#39;, levels = np.arange(-1700, -1900, -10)) p1ar_tops.plot(ax = ax, markersize = 25, label = \u0026#39;wells\u0026#39;, zorder=10) seismic_gdf.plot(ax = ax, color = \u0026#39;black\u0026#39;, label = \u0026#39;seismic lines\u0026#39;) ax.legend() ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) Ну и пожалуй последнее, что мы сделаем сегодня — снимем значения времен с карты изохрон вдоль какого-нибудь сейсмического профиля, для того, чтобы нанести их на сейсмический разрез. Решить данную задачу можно по аналогии с тем, как мы делали это для скважин:\n1 2 3 4 5 6 7 8 9 10 11 12 # Подготовим два массива с координатами профиля в формате xarray x_picket = xr.DataArray(seismic_gdf.iloc[4].geometry.coords.xy[0], dims=\u0026#39;points\u0026#39;) y_picket = xr.DataArray(seismic_gdf.iloc[4].geometry.coords.xy[1], dims=\u0026#39;points\u0026#39;) # Формируем запрос для получения значений времен из массива xarray по координатам time_section = time_grid_xr.sel(x = x_picket, y = y_picket, method=\u0026#39;nearest\u0026#39;) fig, ax = plt.subplots(figsize=(9,6)) seismic_xr.plot(vmin=-40, vmax=40, cmap=\u0026#39;RdBu_r\u0026#39;, ax=ax) ax.plot(time_section-35, \u0026#39;--\u0026#39;, label=\u0026#39;P1ar_time\u0026#39;) ax.legend() ax.grid(True, linestyle=\u0026#39;dotted\u0026#39;) Заключение\rНа сегодня все. Подведем итоги. Сегодня мы:\nНаучились читать табличные данные из файла CSV-формата c помощью Python и библиотеки Pandas Вспомнили базу из геоинформатики и поговорили о геопространственных данных Научились превращать скучные таблицы в пространственные с помощью библиотеки GeoPandas Создали свой первый shp-файл Загрузили свой первый shp-файл Научились работать с поверхностями формата Z-Map Познакомились с библиотекой Xarray, разобрались почему этот формат хорош для работы с гридами Научились извлекать информацию из грида используя координаты Вплотную подошли к основам анализа данных — линейной регрессии. Договорились вернуться к ней позже Разобрались как устроен формат SEG-Y. Научились работать с сейсмическими разрезами с помощью библиотеки ObsPy Автоматизировали нашу первую рутинную задачу — написали шныря по папкам, который умеет строить схему изученности данными сейсморазведки МОГТ-2D. Да, он все еще не идеален, так что не забываем про домашнее задание. Основную цель, которую мы преследовали при подготовке вводного урока по «DIY» — показать, что навыки программирования сегодня являются своего рода суперсилой для современного ученого и инженера, а также дать заинтересованным хорошую отправную точку для экспериментов со своими данными и подготовить читателя к нашей постоянной рубрике «очумелые ручки».\nБезусловно, не весь спектр данных был освещен в данной заметке. В следующих уроках мы обязательно разберемся с данными сейсморазведки МОГТ-3D и поговорим про работу с данными геофизических исследований в скважинах (ГИС).\nNB: Если вы до сих пор тратите часы на монотонные клики в интерфейсе, значит, вы уже отстаёте. Пора брать контроль над данными в свои руки — и Python ваш лучший инструмент для этого. Не забывайте: в эпоху big data и искусственного «интеллекта» выживут только те, кто умеет говорить на языке машин.\n«Шалость удалась!»\n","date":"2025-06-12T00:00:00Z","image":"http://localhost:1313/p/python-%D0%B2-%D0%B3%D0%B5%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D0%B8-%D0%B8-%D0%B3%D0%B5%D0%BE%D1%84%D0%B8%D0%B7%D0%B8%D0%BA%D0%B5/pyhton-in-geoscience_hu_639f9db7e2f4264f.png","permalink":"http://localhost:1313/p/python-%D0%B2-%D0%B3%D0%B5%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D0%B8-%D0%B8-%D0%B3%D0%B5%D0%BE%D1%84%D0%B8%D0%B7%D0%B8%D0%BA%D0%B5/","title":"Python в геологии и геофизике"},{"content":"Мы — сообщество инженеров, гиков и бунтарей, которые взламывают систему изнутри. Наш принцип прост: наука должна быть открытой, как код, и дерзкой, как панк-рок. Мы — те, кто рвут шаблоны и сжигают гранты на кострах из методичек. Наша лаборатория — гаражи и подвалы, наш инструментарий — самописный софт и паяльники. Наши статьи публикуются на GitHub, а не в помойках Elsevier. Здесь не целуют жопу рецензентам. Не молятся на h-index. Не торгуют данными, как барыги. Здесь делают науку, от которой корпорации бледнеют, а академики хватаются за валидол.\nНАУКА ДОЛЖНА БЫТЬ ОТКРЫТОЙ. ДАННЫЕ ДОЛЖНЫ БЫТЬ СВОБОДНЫМИ\r«Если метод нельзя проверить — это не наука, а догма. Если код нельзя запустить — это не инструмент, а «черный ящик». Если вы прячете данные за NDA — вы не учёный, вы стюардесса, раздающая пледы в бизнес-классе.\nНаш принцип: Мы отвергаем «бумажную геофизику», где алгоритмы прячут за патентами, а данные гниют в архивах нефтяных корпораций. Наши данные имеют лицензию «Do What The Fuck You Want». Пока вы тратите миллионы на «инновации» корпораций, продающих вам «костыли» вместо инструментов, мы находим решение скриптом за 50 строк кода.\nНаша цель: Sci-Hub был прав. Мы идем дальше!\nНАУКА — НЕ ХРАМ, А МАСТЕРСКАЯ. «DIY» or DIE\r«Современная наука — храм, в котором данные лежат под стеклом, как святые мощи. Методы — священные ритуалы, а технологии – артефакты, которые работают по воле «богов». Храм, где жрецы в мантиях шепчут: «Это не по методичке…», где гранты дают за послушание, а не за идеи».\nНаш принцип: вот мастерская, вот инструменты, вот чертежи. Создавай, разбирай, улучшай, копируй — и передай следующему.\nНаша цель: Open-source код, открытые данные, воспроизводимые результаты.\nАКАДЕМИКИ — ЭТО НОВЫЕ ЖРЕЦЫ. ДОЛОЙ НАУЧНУЮ ИЕРАРХИЮ\r«Они говорят «это сложно», когда не понимают. Говорят «это невозможно», когда боятся. Говорят «нужны дополнительные исследования», когда просто хотят денег. Их диссертации — это фанфики для бюрократов. Их h-index — взаимные цитирования + цитирования по ошибке»\nНаш принцип: Ученая степень и 20 лет стажа не являются истиной в последней инстанции. Мы уважаем опыт, но отвергаем догмы. Крутая идея может прийти от студента или энтузиаста-самоучки.\nНаша цель: МЕРИТОКРАТИЯ – где ценятся аргументы, а не зВания.\nСТЕРЕОТИПЫ — ЭТО МЕНТАЛЬНЫЙ МУСОР, И МЫ ВЫВОЗИМ ЕГО НА СВАЛКУ\r«Научные стереотипы — это интеллектуальное рабство: методички вместо мыслей, цитаты вместо доказательств, шаблоны вместо открытий. Главное преступление против науки — выдавать тухлые отчёты по шаблону (которые читают только книжные клопы архивов), дрожать перед новыми идеями и прятать трусость за фразой «так принято».\nНаш принцип: Твои результаты не влезают в «стандарты»? Нахуй стандарты! Цитируем только два источника: свой опыт и чужие ошибки. Рецензируем друг друга в баре.\nНаша цель: не бунт, а саботаж: делаем работу не «как надо», а как эффективно.\nСООБЩЕСТВО БОЛЬШЕ, ЧЕМ КОРПОРАЦИИ. НАУКА — ЭТО КОЛЛЕКТИВ ПАНК-РОК ГРУППА\r«Корпорации говорят: «Платите за доступ». Академики шепчут: «Цитируйте друг друга». Мы отвечаем: «Идите нахуй. Наука — это коллективный джем-сейшн, где каждый может ворваться со своей гитарой и устроить научный перформанс».\nНаш принцип: Никаких солистов — все участники группы равны, будь ты профессор или студент-первокурсник. Никаких продюсеров — Schlumberger, Elsevier или РАН не диктуют нам, что играть. Концерты в подвалах — лучшие идеи рождаются не на конференциях за 1000$ «с носа», а в гаражах, чатах и на кухнях в 3 часа ночи. Если код или данные можно взять и переработать — это не воровство, это кавер.\nНаша цель: создать научную сцену, где: статьи распространяются как панк-зины (бесплатно, без цензуры, с опечатками). Эксперименты проходят с таким же адреналином, как концерт группы The Offspring. Корпорации — это не спонсоры, а groupies, которых мы не пускаем за кулисы.\nКорпорации продают науку как поп-музыку — гладкую, коммерческую и пустую. Мы играем панк: грязно, громко и без разрешения. Присоединяйся к банде — или оставайся в толпе зрителей.\nПопробуйте остановить нас — будет весело!\nSCIENCE PUNKS NOT DEAD!\n","date":"2025-03-18T00:00:00Z","image":"http://localhost:1313/p/scipunk-manifesto/scipunk_manifesto_hu_b334000a5f037f00.png","permalink":"http://localhost:1313/p/scipunk-manifesto/","title":"SciPunk Manifesto"}]