Desde hace un tiempo, los datos basados en archivos se han estado migrando a la web y todos estamos acostumbrados a descargar manualmente lo que necesitamos y continuar desde allí en nuestro software de escritorio como ArcGIS Pro.<\/P>
Este blog trata sobre automatizar<\/STRONG> esa experiencia, reemplazando los pasos de descarga y procesamiento con herramientas fáciles de entender, bien definidas y compartibles que entregan tu producto de información bajo demanda<\/STRONG>.<\/P>En dos extremos de la escala de datos tengo divisiones políticas mundiales y actividad de permisos de construcción en Vancouver, Canadá. El primer conjunto de datos es grande<\/STRONG> y proviene de un bucket AWS S3, el segundo es pequeño<\/STRONG> y proviene de un portal de datos abiertos. Ambos se ingieren usando notebooks en ArcGIS Pro con las mismas herramientas básicas.<\/STRONG><\/P>Primero el conjunto de datos pequeño:<\/P>
Permisos de construcción en Vancouver<\/span><\/span><\/P>El conjunto de datos grande:<\/P>
Divisiones políticas mundiales<\/span><\/span><\/P>Y en el punto intermedio, las divisiones políticas solo para Alemania:<\/P>
Una vista de divisiones políticas para Alemania<\/span><\/span><\/P>Todos estos conjuntos de datos fueron ingeridos de manera similar pero con diferencias notables que discutiremos a continuación.<\/P>Si no estás familiarizado con GeoParquet<\/A>, lo estarás, y estoy haciendo que sea mi formato well-known<\/STRONG> preferido para esta publicación. Otros tipos comunes de archivos en la nube como CSV, Excel y JSON también funcionarían. Por "nube" me refiero no solo a dónde vas a obtenerlos sino a formatos que se prestan a consultas remotas sin que tengas que descargar e inspeccionar una copia local.<\/P>Aunque digo que las mismas herramientas básicas pueden acceder a los datos grandes en S3 y a los datos pequeños en un sitio web<\/STRONG>, las capacidades de los servidores respectivos importan. S3 sabe cómo entregar archivos según consultas contra almacenamiento hive, mientras que un sitio web solo sabe cómo entregar descargas de archivos completos.<\/P>Veamos cómo abordar primero el conjunto de datos más pequeño - permisos de construcción, mediante descarga automatizada desde un sitio web.<\/P>Los datos residen en un portal de datos abiertos<\/STRONG><\/A>, si navegas por el sitio verás opciones para formatos de descarga.<\/P>Los datos están automatizados en el notebook VancouverBuildingPermits<\/STRONG>. Si inspeccionas el código notarás que estoy usando DuckDB<\/A> para ayudar - es una base de datos SQL habilitada espacialmente y cliente web. Verás que la descarga y conversión a clase de entidad es muy simple - las herramientas infieren el esquema desde la fuente Parquet más las sentencias SQL<\/STRONG>.<\/P> <\/P>import arcpy
from arcgis.features import GeoAccessor, GeoSeriesAccessor
import duckdb
import math
import os
arcpy.env.overwriteOutput = True
url = r"https:\/\/opendata.vancouver.ca\/api\/explore\/v2.1\/catalog\/datasets\/issued-building-permits\/exports\/parquet?lang=en&refine=issuedate%3A%222024%22&timezone=America%2FLos_Angeles"
conn = duckdb.connect()
conn.sql("set force_download=true;")
conn.sql("install httpfs;load httpfs;")
conn.sql("install spatial;load spatial;")
conn.sql(f"create temp view permitsView as select * from read_parquet('{url}');")
bldgPermits = conn.sql("""select permitnumber as Permit_Number,
try_cast (permitnumbercreateddate as date) as Permit_Number_Created_Date,
try_cast (issuedate as date) as Issue_Date,
permitelapseddays::int as Permit_Elapsed_Days,
projectvalue::float as Project_Value,
typeofwork as Type_Of_Work,
address as Address,
projectdescription as Project_Description,
permitcategory as Permit_Category,
applicant as Applicant,
applicantaddress as Applicant_Address,
propertyuse as Property_Use,
specificusecategory as Specific_Use_Category,
buildingcontractor as Building_Contractor,
buildingcontractoraddress as Building_Contractor_Address,
issueyear as Issue_Year,
geolocalarea as Geo_Loc_Area,
yearmonth as Year_Month,
ST_AsText(ST_GeomFromWKB(geom)) as SHAPE
from permitsView;""")
df = bldgPermits.df()
df.spatial.set_geometry("SHAPE",sr=4326,inplace=True)
aprx = arcpy.mp.ArcGISProject("CURRENT")
gdb = aprx.defaultGeodatabase
out_fc = "Issued_Building_Permits"
location = os.path.join(gdb,out_fc)
df.spatial.to_featureclass(location,sanitize_columns=False)<\/code><\/pre><P> <\/P><P>Si inspeccionas el código en la descarga verás algunos pasos extra que usé para redondear anchos de campos texto a factores de 10, un adorno innecesario en este caso pero solo para mostrarte cómo hacer esto si esperas que los datos sean editados en el futuro y no quieres truncamiento de valores.<\/P><P>Así que ese es un caso simple de ingerir un archivo pequeño residente en la nube dentro de ArcGIS con control mínimo sobre el esquema - nota nuevamente que sentencias SQL estándar están disponibles para definir tu vista del archivo originado en la nube. Compara este enfoque con definir el esquema usando geoprocesamiento en <A title="Refining the schema for local well-known files" href="https:\/\/community.esri.com\/t5\/etl-patterns-blog\/etl-pattern-working-with-local-well-known-files\/ba-p\/1515000" target="_blank">esta publicación relacionada<\/A>.<\/P><P>Pasemos a datos a escala global usando el notebook <STRONG>Division_Area<\/STRONG>, usando datos del <A title="Overture" href="https:\/\/overturemaps.org\/download\/" target="_blank" rel="noopener nofollow noreferrer">Overture Maps Foundation<\/A>.<\/P><P> <\/P><pre class="lia-code-sample language-python"><code># Obtener la capa Division_Area del Overture Maps Foundation
import arcpy
from datetime import datetime
import duckdb
import os
aprx = arcpy.mp.ArcGISProject("CURRENT")
homeFolder = aprx.homeFolder
gdb = aprx.defaultGeodatabase
release = "2024-07-22.0"
def getNow():
return str(datetime.utcnow().replace(microsecond=0))
arcpy.env.overwriteOutput = True
sR = arcpy.SpatialReference(4326)
conn = duckdb.connect()
conn.sql("install spatial;load spatial;")
conn.sql("install httpfs;load httpfs;")
conn.sql("set s3_region='us-west-2';")
conn.sql("set enable_object_cache=true;")
# Crear la relación DuckDB
# Inserta cualquier cláusula where deseada, por ejemplo "country = 'GB'"
# La expresión order-by ayuda a mostrar divisiones pequeñas encima de las grandes, pero es costosa
print(f"Procesamiento comenzando a las {getNow()}")
whereExp = "1 = 1"
sql = f"""select id,
bbox.xmin as xmin,
bbox.ymin as ymin,
bbox.xmax as xmax,
bbox.ymax as ymax,
version,
subtype,
names.primary as primary_name,
class,
region,
country,
norms.driving_side as driving_side,
geometry
from read_parquet('s3:\/\/_overturemaps-us-west-2\/_release\/{release}\/_theme=divisions\/_type=division_area\/*',filename=true, hive_partitioning=1)
where {whereExp}
order by ST_Area(ST_GeomFromWKB(geometry)) desc;"""
duckDivisions = conn.sql(sql)
# Crear la clase entidad salida con control preciso del esquema
print('Creando clase entidad salida en {}'.format(getNow()))
arсDivisions = arcpy.management.CreateFeatureclass(out_path=gdb,
out_name="Division_Areas",
geometry_type="POLYGON",
spatial_reference=sR).getOutput(0)
arcpy.management.AddField(in_table=arcDivisions,field_name="id",field_type="TEXT",field_length=32)
for f in ["xmin","ymin","xmax","ymax"]:
arcpy.management.AddField(in_table=arcDivisions,field_name=f,field_type="FLOAT")
arcpy.management.AddField(in_table=arcDivisions,field_name="version",field_type="SHORT")
arcpy.management.AddField(in_table=arcDivisions,field_name="subtype",field_type="TEXT",field_length=20)
arcpy.management.AddField(in_table=arcDivisions,field_name="primary_name",field_type="TEXT",field_length=100)
arcpy.management.AddField(in_table=arcDivisions,field_name="class",field_type="TEXT",field_length=6)
arcpy.management.AddField(in_table=arcDivisions,field_name="region",field_type="TEXT",field_length=10)
arcpy.management.AddField(in_table=arcDivisions,field_name="country",field_type="TEXT",field_length=2)
arcpy.management.AddField(in_table=arcDivisions,field_name="driving_side",field_type="TEXT",field_length=10)
# Escribir la salida
print('Escribiendo salida en {}'.format(getNow()))
con arcpy.da.InsertCursor(arcDivisions,["id","xmin","ymin","xmax","ymax","version",
"subtype","primary_name","class","region",
"country","driving_side","shape@"]) como iCursor:
fila = duckDivisions.fetchone()
i = 1
mientras fila:
si i % 100000 == 0:
print(f"Insertadas {i} filas de Division_Areas...")
fila = list(fila)
fila[-1] = arcpy.FromWKB(fila[-1])
iCursor.insertRow(fila)
i+=1
fila = duckDivisions.fetchone()
del iCursor
# Reparar geometrías, eliminar geometrías nulas
print(f"Corrigiendo geometrías incorrectas en {getNow()}")
arcpy.management.RepairGeometry(
in_features=arcDivisions,
delete_null="DELETE_NULL",
validation_method="OGC")
print(f"Finalizado en {getNow()}")
Las principales diferencias entre esta ingestión de datos a escala global y la de los permisos de construcción es que la fuente se obtiene mediante una consulta comodín en el almacenamiento hive de S3, y el esquema se define manualmente. La herramienta es la misma.< /P>
Así que esta es una mirada a la ingestión de datos enfocada en el usuario a gran y pequeña escala, pero ¿qué pasa si eres un propietario de datos y quieres ofrecer una entrega de datos basada en archivos well-known en la nube infinitamente flexible para una amplia gama de usuarios sin que tengan que molestarse en construir herramientas? Aquí es donde entra el ejemplo a media escala de divisiones políticas para Alemania.< /P>
La biblioteca cliente que estamos usando - DuckDB - también es un formato de almacenamiento, pero uno con capacidades de base de datos espacial, incluyendo vistas, así que para el caso de datos provenientes de la nube es sencillo ofrecer un producto de datos que contiene solo una vista que cualquier usuario final puede leer bajo demanda. Esto significa que, con el esfuerzo de definir una vista, un producto de datos de cualquier escala puede entregarse en un archivo de base de datos pequeño.< /P>
La descarga del blog contiene tal producto de datos - mydivisionview.duckdb - que fue creado por el cuaderno MakeViewDatabase y puede ser ingerido por cualquiera usando el cuaderno ReadViewDatabase. En este caso entonces un archivo de solo 268KB puede entregar datos a escala nacional (o incluso global). Este es el poder de los archivos well-known nativos en la nube leídos bajo demanda desde la nube. Si la fuente del conjunto de datos cambia, la misma base DuckDB siempre accederá a los datos más recientes.< /P>
A continuación está el código para ReadViewDatabase. Si copias mydivisionview.duck db en tu carpeta del proyecto entregará el contenido de la vista en tu geodatabase predeterminada del proyecto en segundos (15 en mi máquina):
import arcpy
from datetime import datetime
import duckdb
import os
def getNow():
return str(datetime.utcnow().replace(microsecond=0))
print(f"Extrayendo datos en {getNow()}")
arcpy.env.overwriteOutput = True
aprx = arcpy.mp.ArcGISProject("CURRENT")
casaCarpeta = aprx.homeFolder
gdb = aprx.defaultGeodatabase
sR = arcpy.SpatialReference(4326)
duckDB = os.path.join(casaCarpeta,"MyDivisionView.duckdb")
conn = duckdb.connect(duckDB)
conn.sql("install spatial;load spatial;")
conn.sql("install httpfs;load httpfs;")
conn.sql("set enable_object_cache=true;")
duckDivisions = conn.sql(f"""select id,
xmin::float as xmin,
ymin::float as ymin,
xmax::float as xmax,
ymax::float as ymax,
version::short as version,
subtype,
primary_name,
class,
region,
country,
driving_side,
geometry
from MyDivisionView;""")
arcpDivisions = arcpy.management.CreateFeatureclass(gdb,
out_name="My_Division_Areas",
geometry_type="POLYGON",
spatial_reference=sR).getOutput(0)
arcpy.management.AddField(in_table=arcDivisions,field_name="id",field_type="TEXT",field_length=32)
para f en ["xmin","ymin","xmax","ymax"]:
arcpy.management.AddField(in_table=arcDivisions,field_name=f,field_type="FLOAT")
arcpy.management.AddField(in_table=arcDivisions,field_name="version",field_type="SHORT")
arcpy.management.AddField(in_table=arcDivisions,field_name="subtype",field_type="TEXT",field_length=20)
arcpy.management.AddField(in_table=arcDivisions,field_name="primary_name",field_type="TEXT",field_length=100)
arcpy.management.AddField(in_table=arcDivisions,field_name="class",field_type="TEXT",field_length=6)
arcpy.management.AddField(in_table=arcDivisions,field_name="region",field_type="TEXT",field_length=10)
arcpy.management.AddField(in_table=arcDivisions,field_name="country",field_type="TEXT",field_length=2)
arcpy.management.AddField(in_table=arcDivisions,field_name="driving_side",field_type="TEXT",field_length=10)
con arcpy.da.InsertCursor(arcDivisions,["id","xmin","ymin","xmax","ymax","version","subtype",
"primary_name","class","region","country",
"driving_side","shape@"]) como iCursor:
fila = duckDivisions.fetchone()
i = 1
mientras fila:
#si i % 1000 == 0:
# print('Insertadas {} filas My_Division_Area'.format(i))
fila = list(fila)
fila[-1] = arcpy.FromWKB(fila[-1])
iCursor.insertRow(fila)
i+=1
fila = duckDivisions.fetchone()
del iCursor
conn.close()
print(f"Datos extraídos en {getNow()}")Terminaré con algunos puntos resumidos:< /P>
- Los archivos well-known pequeños en la nube son fácilmente ingeridos.< /LI>
- Los archivos well-known ilimitados a escala en almacenamiento de objetos son fácilmente ingeridos.< /LI>
- Cualquier dato well-known a cualquier escala en cualquier definición de vista puede compartirse fácilmente en contenedores pequeños.< /LI>
< /P>
< /P>
< /P>