[{"data":1,"prerenderedAt":2210},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fload-geodataframe-into-qgis-pyqgis":3},{"id":4,"title":5,"body":6,"description":2200,"extension":2201,"meta":2202,"navigation":232,"path":2206,"seo":2207,"stem":2208,"__hash__":2209},"docs\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fload-geodataframe-into-qgis-pyqgis\u002Findex.md","Load a GeoDataFrame into QGIS",{"type":7,"value":8,"toc":2186},"minimark",[9,13,17,26,159,164,179,183,186,430,445,449,452,533,1153,1196,1200,1203,1277,1291,1295,1298,1360,1681,1690,1694,1697,1925,1945,1949,1952,2021,2036,2040,2060,2064,2103,2107,2110,2114,2127,2133,2139,2148,2152,2182],[10,11,5],"h1",{"id":12},"load-a-geodataframe-into-qgis",[14,15,16],"p",{},"The other half of working with GeoPandas from QGIS is bringing results back: the buffer-and-dissolve you did in pandas, the frame read from a web API, the output of a scikit-learn model with a geometry column. QGIS needs a layer to display, style and export, and there are two good ways to make one — through a file, which is simple and robust, or directly into a memory layer, which avoids disk and keeps everything in the session.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002F","PyQGIS and the Python Data Stack",". It covers both routes, maps pandas dtypes to QGIS field types, deals with mixed geometry types, NULLs and dates, and applies a style so the new layer is immediately readable.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,90,98,102,108,113,118,123,128,132,135,138,143,146,154],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","A GeoDataFrame reaching QGIS either through a GeoPackage file loaded as a layer or directly as a memory layer built from its dtypes and WKB geometry","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Two routes from a GeoDataFrame to a layer",[40,41,42],"desc",{},"Route one: write the frame to a GeoPackage with to_file and load it as an ogr layer; robust, persistent, and type mapping is done by GDAL. Route two: build a memory layer whose fields are derived from the frame's dtypes, convert each geometry to WKB and add features in one batch; no disk, but types and geometry must be mapped by hand. Either way the layer is added to the project and styled.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gdfBackArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Through a file, or straight into memory",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"290","44","180","52","#fffdf7","#59645f","stroke-width:2",[68,86,89],{"x":70,"y":87,"style":88,"fill":73,"textAnchor":74},"73.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","GeoDataFrame",[91,92],"line",{"x1":93,"y1":94,"x2":95,"y2":96,"stroke":66,"style":97},"330","96","190","126","stroke-width:1.8;marker-end:url(#gdfBackArrow)",[91,99],{"x1":100,"y1":94,"x2":101,"y2":96,"stroke":66,"style":97},"430","570",[44,103],{"x":104,"y":105,"width":93,"height":94,"rx":58,"fill":106,"stroke":107,"style":84},"24","130","#eef7f4","#0f766e",[68,109,112],{"x":110,"y":111,"style":88,"fill":107,"textAnchor":74},"189","157.78","to_file → GeoPackage",[68,114,117],{"x":110,"y":115,"style":116,"fill":66,"textAnchor":74},"181.78","text-anchor:middle;font-size:10.0px;font-family:monospace","QgsVectorLayer(path, …, \"ogr\")",[68,119,122],{"x":110,"y":120,"style":121,"fill":83,"textAnchor":74},"205.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","persistent · GDAL maps types",[44,124],{"x":125,"y":105,"width":93,"height":94,"rx":58,"fill":126,"stroke":127,"style":84},"406","#eff3ff","#2563eb",[68,129,131],{"x":130,"y":111,"style":88,"fill":127,"textAnchor":74},"571","memory layer",[68,133,134],{"x":130,"y":115,"style":116,"fill":66,"textAnchor":74},"fields from dtypes · WKB",[68,136,137],{"x":130,"y":120,"style":121,"fill":83,"textAnchor":74},"no disk · you map types",[91,139],{"x1":95,"y1":140,"x2":141,"y2":142,"stroke":66,"style":97},"226","296","250",[91,144],{"x1":101,"y1":140,"x2":145,"y2":142,"stroke":66,"style":97},"464",[44,147],{"x":148,"y":149,"width":150,"height":151,"rx":58,"fill":152,"stroke":153,"style":84},"300","238","160","36","#e8efe6","#15803d",[68,155,158],{"x":70,"y":156,"style":88,"fill":157,"textAnchor":74},"259.78","#166534","layer on the map",[160,161,163],"h2",{"id":162},"prerequisites","Prerequisites",[165,166,167,171],"ul",{},[168,169,170],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with GeoPandas and shapely 2 installed into QGIS's Python.",[168,172,173,174,178],{},"A GeoDataFrame with a CRS set. Check ",[175,176,177],"code",{},"gdf.crs"," before anything else; a frame without a CRS produces a layer QGIS will place wherever it guesses.",[160,180,182],{"id":181},"route-1-through-a-geopackage","Route 1: through a GeoPackage",[14,184,185],{},"Writing to a file and loading it is the most robust route. GDAL handles type mapping, the result survives a restart, and the same file is readable by every other tool.",[187,188,193],"pre",{"className":189,"code":190,"language":191,"meta":192,"style":192},"language-python shiki shiki-themes github-dark","import geopandas as gpd\nfrom qgis.core import QgsVectorLayer, QgsProject\n\ngdf = gpd.read_file(\"\u002Fdata\u002Finbox\u002Fsites.geojson\").to_crs(25832)\ngdf[\"buffer_m\"] = 250\ngdf[\"geometry\"] = gdf.buffer(gdf[\"buffer_m\"])\n\npath = \"\u002Fdata\u002Fresults\u002Fanalysis.gpkg\"\ngdf.to_file(path, layer=\"site_buffers\", driver=\"GPKG\")\n\nlayer = QgsVectorLayer(f\"{path}|layername=site_buffers\", \"site buffers\", \"ogr\")\nif not layer.isValid():\n    raise RuntimeError(\"could not load the written layer\")\nQgsProject.instance().addMapLayer(layer)\nprint(layer.featureCount(), layer.crs().authid(), layer.fields().names())\n","python","",[175,194,195,213,227,234,260,277,297,302,313,341,346,386,398,415,421],{"__ignoreMap":192},[196,197,199,203,207,210],"span",{"class":91,"line":198},1,[196,200,202],{"class":201},"snl16","import",[196,204,206],{"class":205},"s95oV"," geopandas ",[196,208,209],{"class":201},"as",[196,211,212],{"class":205}," gpd\n",[196,214,216,219,222,224],{"class":91,"line":215},2,[196,217,218],{"class":201},"from",[196,220,221],{"class":205}," qgis.core ",[196,223,202],{"class":201},[196,225,226],{"class":205}," QgsVectorLayer, QgsProject\n",[196,228,230],{"class":91,"line":229},3,[196,231,233],{"emptyLinePlaceholder":232},true,"\n",[196,235,237,240,243,246,250,253,257],{"class":91,"line":236},4,[196,238,239],{"class":205},"gdf ",[196,241,242],{"class":201},"=",[196,244,245],{"class":205}," gpd.read_file(",[196,247,249],{"class":248},"sU2Wk","\"\u002Fdata\u002Finbox\u002Fsites.geojson\"",[196,251,252],{"class":205},").to_crs(",[196,254,256],{"class":255},"sDLfK","25832",[196,258,259],{"class":205},")\n",[196,261,263,266,269,272,274],{"class":91,"line":262},5,[196,264,265],{"class":205},"gdf[",[196,267,268],{"class":248},"\"buffer_m\"",[196,270,271],{"class":205},"] ",[196,273,242],{"class":201},[196,275,276],{"class":255}," 250\n",[196,278,280,282,285,287,289,292,294],{"class":91,"line":279},6,[196,281,265],{"class":205},[196,283,284],{"class":248},"\"geometry\"",[196,286,271],{"class":205},[196,288,242],{"class":201},[196,290,291],{"class":205}," gdf.buffer(gdf[",[196,293,268],{"class":248},[196,295,296],{"class":205},"])\n",[196,298,300],{"class":91,"line":299},7,[196,301,233],{"emptyLinePlaceholder":232},[196,303,305,308,310],{"class":91,"line":304},8,[196,306,307],{"class":205},"path ",[196,309,242],{"class":201},[196,311,312],{"class":248}," \"\u002Fdata\u002Fresults\u002Fanalysis.gpkg\"\n",[196,314,316,319,323,325,328,331,334,336,339],{"class":91,"line":315},9,[196,317,318],{"class":205},"gdf.to_file(path, ",[196,320,322],{"class":321},"s9osk","layer",[196,324,242],{"class":201},[196,326,327],{"class":248},"\"site_buffers\"",[196,329,330],{"class":205},", ",[196,332,333],{"class":321},"driver",[196,335,242],{"class":201},[196,337,338],{"class":248},"\"GPKG\"",[196,340,259],{"class":205},[196,342,344],{"class":91,"line":343},10,[196,345,233],{"emptyLinePlaceholder":232},[196,347,349,352,354,357,360,363,366,368,371,374,376,379,381,384],{"class":91,"line":348},11,[196,350,351],{"class":205},"layer ",[196,353,242],{"class":201},[196,355,356],{"class":205}," QgsVectorLayer(",[196,358,359],{"class":201},"f",[196,361,362],{"class":248},"\"",[196,364,365],{"class":255},"{",[196,367,63],{"class":205},[196,369,370],{"class":255},"}",[196,372,373],{"class":248},"|layername=site_buffers\"",[196,375,330],{"class":205},[196,377,378],{"class":248},"\"site buffers\"",[196,380,330],{"class":205},[196,382,383],{"class":248},"\"ogr\"",[196,385,259],{"class":205},[196,387,389,392,395],{"class":91,"line":388},12,[196,390,391],{"class":201},"if",[196,393,394],{"class":201}," not",[196,396,397],{"class":205}," layer.isValid():\n",[196,399,401,404,407,410,413],{"class":91,"line":400},13,[196,402,403],{"class":201},"    raise",[196,405,406],{"class":255}," RuntimeError",[196,408,409],{"class":205},"(",[196,411,412],{"class":248},"\"could not load the written layer\"",[196,414,259],{"class":205},[196,416,418],{"class":91,"line":417},14,[196,419,420],{"class":205},"QgsProject.instance().addMapLayer(layer)\n",[196,422,424,427],{"class":91,"line":423},15,[196,425,426],{"class":255},"print",[196,428,429],{"class":205},"(layer.featureCount(), layer.crs().authid(), layer.fields().names())\n",[14,431,432,436,437,440,441,444],{},[433,434,435],"strong",{},"Breakdown:"," ",[175,438,439],{},"to_file"," with the GPKG driver writes one table into the GeoPackage, creating the file if needed and replacing that table if it exists. The ",[175,442,443],{},"|layername="," suffix tells the OGR provider which table to open — necessary because a GeoPackage can hold many. The CRS travels inside the file, so QGIS reads exactly what GeoPandas wrote. This route is the right default for results anyone else will use, and for anything large: GeoPackage writing is fast and the layer gets a proper spatial index.",[160,446,448],{"id":447},"route-2-straight-into-a-memory-layer","Route 2: straight into a memory layer",[14,450,451],{},"For intermediate results inside a session — something to look at, style and throw away — a memory layer avoids files altogether. The work is mapping the frame's columns to QGIS field types and its geometry to WKB.",[14,453,454],{},[29,455,458,461,464,466,473,476,480,485,489,493,497,501,505,508,512,515,518,521,524,527],{"viewBox":456,"role":32,"ariaLabel":457,"xmlns":34},"0 0 760 250","Mapping of pandas dtypes to QGIS memory layer field types, with a string fallback for anything unknown",[36,459,460],{},"Pandas dtypes to QGIS field types",[40,462,463],{},"A mapping table from pandas dtypes to memory layer field types: integer dtypes including nullable Int64 map to int8 for 64-bit safety, floats to double, booleans to bool, datetime64 to datetime, and object or string to string. Unknown dtypes fall back to string so no column is dropped.",[44,465],{"x":46,"y":46,"width":47,"height":142,"fill":49},[51,467,468],{},[54,469,471],{"id":470,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gdfTypesArrow",[63,472],{"d":65,"fill":66},[68,474,475],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"One rule per dtype, string as the fallback",[44,477],{"x":104,"y":81,"width":478,"height":479,"rx":58,"fill":82,"stroke":83,"style":84},"340","176",[68,481,484],{"x":482,"y":483,"style":88,"fill":73,"textAnchor":74},"194","78.78","pandas dtype",[68,486,488],{"x":482,"y":487,"style":121,"fill":66,"textAnchor":74},"104.78","int64 \u002F Int64",[68,490,492],{"x":482,"y":491,"style":121,"fill":66,"textAnchor":74},"130.78","float64",[68,494,496],{"x":482,"y":495,"style":121,"fill":66,"textAnchor":74},"156.78","bool \u002F boolean",[68,498,500],{"x":482,"y":499,"style":121,"fill":66,"textAnchor":74},"182.78","datetime64",[68,502,504],{"x":482,"y":503,"style":121,"fill":66,"textAnchor":74},"208.78","object \u002F string",[44,506],{"x":507,"y":81,"width":478,"height":479,"rx":58,"fill":106,"stroke":107,"style":84},"396",[68,509,511],{"x":510,"y":483,"style":88,"fill":107,"textAnchor":74},"566","memory layer type",[68,513,514],{"x":510,"y":487,"style":121,"fill":66,"textAnchor":74},"int8 (64-bit)",[68,516,517],{"x":510,"y":491,"style":121,"fill":66,"textAnchor":74},"double",[68,519,520],{"x":510,"y":495,"style":121,"fill":66,"textAnchor":74},"bool",[68,522,523],{"x":510,"y":499,"style":121,"fill":66,"textAnchor":74},"datetime",[68,525,526],{"x":510,"y":503,"style":121,"fill":66,"textAnchor":74},"string",[91,528],{"x1":529,"y1":530,"x2":531,"y2":530,"stroke":66,"style":532},"364","140","392","stroke-width:1.8;marker-end:url(#gdfTypesArrow)",[187,534,536],{"className":189,"code":535,"language":191,"meta":192,"style":192},"import pandas as pd\nfrom qgis.core import QgsFeature, QgsGeometry, QgsCoordinateReferenceSystem\n\ndef qgis_type(dtype):\n    if pd.api.types.is_bool_dtype(dtype):\n        return \"bool\"\n    if pd.api.types.is_integer_dtype(dtype):\n        return \"int8\"\n    if pd.api.types.is_float_dtype(dtype):\n        return \"double\"\n    if pd.api.types.is_datetime64_any_dtype(dtype):\n        return \"datetime\"\n    return \"string\"\n\ndef gdf_to_memory_layer(gdf, name, geom_type=None):\n    gtype = geom_type or gdf.geom_type.dropna().iloc[0]\n    if gdf.geom_type.str.startswith(\"Multi\").any() and not gtype.startswith(\"Multi\"):\n        gtype = \"Multi\" + gtype\n    cols = [c for c in gdf.columns if c != gdf.geometry.name]\n    fields = \"\".join(f\"&field={c}:{qgis_type(gdf[c].dtype)}\" for c in cols)\n    epsg = gdf.crs.to_epsg()\n    layer = QgsVectorLayer(f\"{gtype}?crs=EPSG:{epsg}{fields}\" if epsg else f\"{gtype}{fields.replace('&', '?', 1)}\",\n                           name, \"memory\")\n    if not epsg:\n        layer.setCrs(QgsCoordinateReferenceSystem.fromWkt(gdf.crs.to_wkt()))\n\n    feats = []\n    for row, geom in zip(gdf[cols].itertuples(index=False), gdf.geometry):\n        f = QgsFeature(layer.fields())\n        f.setAttributes([None if pd.isna(v) else (v.to_pydatetime() if isinstance(v, pd.Timestamp) else v)\n                         for v in row])\n        if geom is not None and not geom.is_empty:\n            g = QgsGeometry()\n            g.fromWkb(geom.wkb)\n            if gtype.startswith(\"Multi\"):\n                g.convertToMultiType()\n            f.setGeometry(g)\n        feats.append(f)\n    layer.dataProvider().addFeatures(feats)\n    layer.updateExtents()\n    return layer\n\nmem = gdf_to_memory_layer(gdf, \"site buffers (memory)\")\nQgsProject.instance().addMapLayer(mem)\n",[175,537,538,550,561,565,577,585,593,600,607,614,621,628,635,643,647,665,687,713,730,763,811,822,907,918,928,934,939,950,978,989,1020,1034,1059,1070,1076,1088,1094,1100,1106,1112,1118,1126,1131,1147],{"__ignoreMap":192},[196,539,540,542,545,547],{"class":91,"line":198},[196,541,202],{"class":201},[196,543,544],{"class":205}," pandas ",[196,546,209],{"class":201},[196,548,549],{"class":205}," pd\n",[196,551,552,554,556,558],{"class":91,"line":215},[196,553,218],{"class":201},[196,555,221],{"class":205},[196,557,202],{"class":201},[196,559,560],{"class":205}," QgsFeature, QgsGeometry, QgsCoordinateReferenceSystem\n",[196,562,563],{"class":91,"line":229},[196,564,233],{"emptyLinePlaceholder":232},[196,566,567,570,574],{"class":91,"line":236},[196,568,569],{"class":201},"def",[196,571,573],{"class":572},"svObZ"," qgis_type",[196,575,576],{"class":205},"(dtype):\n",[196,578,579,582],{"class":91,"line":262},[196,580,581],{"class":201},"    if",[196,583,584],{"class":205}," pd.api.types.is_bool_dtype(dtype):\n",[196,586,587,590],{"class":91,"line":279},[196,588,589],{"class":201},"        return",[196,591,592],{"class":248}," \"bool\"\n",[196,594,595,597],{"class":91,"line":299},[196,596,581],{"class":201},[196,598,599],{"class":205}," pd.api.types.is_integer_dtype(dtype):\n",[196,601,602,604],{"class":91,"line":304},[196,603,589],{"class":201},[196,605,606],{"class":248}," \"int8\"\n",[196,608,609,611],{"class":91,"line":315},[196,610,581],{"class":201},[196,612,613],{"class":205}," pd.api.types.is_float_dtype(dtype):\n",[196,615,616,618],{"class":91,"line":343},[196,617,589],{"class":201},[196,619,620],{"class":248}," \"double\"\n",[196,622,623,625],{"class":91,"line":348},[196,624,581],{"class":201},[196,626,627],{"class":205}," pd.api.types.is_datetime64_any_dtype(dtype):\n",[196,629,630,632],{"class":91,"line":388},[196,631,589],{"class":201},[196,633,634],{"class":248}," \"datetime\"\n",[196,636,637,640],{"class":91,"line":400},[196,638,639],{"class":201},"    return",[196,641,642],{"class":248}," \"string\"\n",[196,644,645],{"class":91,"line":417},[196,646,233],{"emptyLinePlaceholder":232},[196,648,649,651,654,657,659,662],{"class":91,"line":423},[196,650,569],{"class":201},[196,652,653],{"class":572}," gdf_to_memory_layer",[196,655,656],{"class":205},"(gdf, name, geom_type",[196,658,242],{"class":201},[196,660,661],{"class":255},"None",[196,663,664],{"class":205},"):\n",[196,666,668,671,673,676,679,682,684],{"class":91,"line":667},16,[196,669,670],{"class":205},"    gtype ",[196,672,242],{"class":201},[196,674,675],{"class":205}," geom_type ",[196,677,678],{"class":201},"or",[196,680,681],{"class":205}," gdf.geom_type.dropna().iloc[",[196,683,46],{"class":255},[196,685,686],{"class":205},"]\n",[196,688,690,692,695,698,701,704,706,709,711],{"class":91,"line":689},17,[196,691,581],{"class":201},[196,693,694],{"class":205}," gdf.geom_type.str.startswith(",[196,696,697],{"class":248},"\"Multi\"",[196,699,700],{"class":205},").any() ",[196,702,703],{"class":201},"and",[196,705,394],{"class":201},[196,707,708],{"class":205}," gtype.startswith(",[196,710,697],{"class":248},[196,712,664],{"class":205},[196,714,716,719,721,724,727],{"class":91,"line":715},18,[196,717,718],{"class":205},"        gtype ",[196,720,242],{"class":201},[196,722,723],{"class":248}," \"Multi\"",[196,725,726],{"class":201}," +",[196,728,729],{"class":205}," gtype\n",[196,731,733,736,738,741,744,747,750,753,755,757,760],{"class":91,"line":732},19,[196,734,735],{"class":205},"    cols ",[196,737,242],{"class":201},[196,739,740],{"class":205}," [c ",[196,742,743],{"class":201},"for",[196,745,746],{"class":205}," c ",[196,748,749],{"class":201},"in",[196,751,752],{"class":205}," gdf.columns ",[196,754,391],{"class":201},[196,756,746],{"class":205},[196,758,759],{"class":201},"!=",[196,761,762],{"class":205}," gdf.geometry.name]\n",[196,764,766,769,771,774,777,779,782,784,787,789,792,794,797,799,801,804,806,808],{"class":91,"line":765},20,[196,767,768],{"class":205},"    fields ",[196,770,242],{"class":201},[196,772,773],{"class":248}," \"\"",[196,775,776],{"class":205},".join(",[196,778,359],{"class":201},[196,780,781],{"class":248},"\"&field=",[196,783,365],{"class":255},[196,785,786],{"class":205},"c",[196,788,370],{"class":255},[196,790,791],{"class":248},":",[196,793,365],{"class":255},[196,795,796],{"class":205},"qgis_type(gdf[c].dtype)",[196,798,370],{"class":255},[196,800,362],{"class":248},[196,802,803],{"class":201}," for",[196,805,746],{"class":205},[196,807,749],{"class":201},[196,809,810],{"class":205}," cols)\n",[196,812,814,817,819],{"class":91,"line":813},21,[196,815,816],{"class":205},"    epsg ",[196,818,242],{"class":201},[196,820,821],{"class":205}," gdf.crs.to_epsg()\n",[196,823,825,828,830,832,834,836,838,841,843,846,848,851,854,857,859,861,864,867,870,873,875,877,879,881,884,887,889,892,894,897,900,902,904],{"class":91,"line":824},22,[196,826,827],{"class":205},"    layer ",[196,829,242],{"class":201},[196,831,356],{"class":205},[196,833,359],{"class":201},[196,835,362],{"class":248},[196,837,365],{"class":255},[196,839,840],{"class":205},"gtype",[196,842,370],{"class":255},[196,844,845],{"class":248},"?crs=EPSG:",[196,847,365],{"class":255},[196,849,850],{"class":205},"epsg",[196,852,853],{"class":255},"}{",[196,855,856],{"class":205},"fields",[196,858,370],{"class":255},[196,860,362],{"class":248},[196,862,863],{"class":201}," if",[196,865,866],{"class":205}," epsg ",[196,868,869],{"class":201},"else",[196,871,872],{"class":201}," f",[196,874,362],{"class":248},[196,876,365],{"class":255},[196,878,840],{"class":205},[196,880,853],{"class":255},[196,882,883],{"class":205},"fields.replace(",[196,885,886],{"class":248},"'&'",[196,888,330],{"class":205},[196,890,891],{"class":248},"'?'",[196,893,330],{"class":205},[196,895,896],{"class":255},"1",[196,898,899],{"class":205},")",[196,901,370],{"class":255},[196,903,362],{"class":248},[196,905,906],{"class":205},",\n",[196,908,910,913,916],{"class":91,"line":909},23,[196,911,912],{"class":205},"                           name, ",[196,914,915],{"class":248},"\"memory\"",[196,917,259],{"class":205},[196,919,921,923,925],{"class":91,"line":920},24,[196,922,581],{"class":201},[196,924,394],{"class":201},[196,926,927],{"class":205}," epsg:\n",[196,929,931],{"class":91,"line":930},25,[196,932,933],{"class":205},"        layer.setCrs(QgsCoordinateReferenceSystem.fromWkt(gdf.crs.to_wkt()))\n",[196,935,937],{"class":91,"line":936},26,[196,938,233],{"emptyLinePlaceholder":232},[196,940,942,945,947],{"class":91,"line":941},27,[196,943,944],{"class":205},"    feats ",[196,946,242],{"class":201},[196,948,949],{"class":205}," []\n",[196,951,953,956,959,961,964,967,970,972,975],{"class":91,"line":952},28,[196,954,955],{"class":201},"    for",[196,957,958],{"class":205}," row, geom ",[196,960,749],{"class":201},[196,962,963],{"class":255}," zip",[196,965,966],{"class":205},"(gdf[cols].itertuples(",[196,968,969],{"class":321},"index",[196,971,242],{"class":201},[196,973,974],{"class":255},"False",[196,976,977],{"class":205},"), gdf.geometry):\n",[196,979,981,984,986],{"class":91,"line":980},29,[196,982,983],{"class":205},"        f ",[196,985,242],{"class":201},[196,987,988],{"class":205}," QgsFeature(layer.fields())\n",[196,990,992,995,997,999,1002,1004,1007,1009,1012,1015,1017],{"class":91,"line":991},30,[196,993,994],{"class":205},"        f.setAttributes([",[196,996,661],{"class":255},[196,998,863],{"class":201},[196,1000,1001],{"class":205}," pd.isna(v) ",[196,1003,869],{"class":201},[196,1005,1006],{"class":205}," (v.to_pydatetime() ",[196,1008,391],{"class":201},[196,1010,1011],{"class":255}," isinstance",[196,1013,1014],{"class":205},"(v, pd.Timestamp) ",[196,1016,869],{"class":201},[196,1018,1019],{"class":205}," v)\n",[196,1021,1023,1026,1029,1031],{"class":91,"line":1022},31,[196,1024,1025],{"class":201},"                         for",[196,1027,1028],{"class":205}," v ",[196,1030,749],{"class":201},[196,1032,1033],{"class":205}," row])\n",[196,1035,1037,1040,1043,1046,1048,1051,1054,1056],{"class":91,"line":1036},32,[196,1038,1039],{"class":201},"        if",[196,1041,1042],{"class":205}," geom ",[196,1044,1045],{"class":201},"is",[196,1047,394],{"class":201},[196,1049,1050],{"class":255}," None",[196,1052,1053],{"class":201}," and",[196,1055,394],{"class":201},[196,1057,1058],{"class":205}," geom.is_empty:\n",[196,1060,1062,1065,1067],{"class":91,"line":1061},33,[196,1063,1064],{"class":205},"            g ",[196,1066,242],{"class":201},[196,1068,1069],{"class":205}," QgsGeometry()\n",[196,1071,1073],{"class":91,"line":1072},34,[196,1074,1075],{"class":205},"            g.fromWkb(geom.wkb)\n",[196,1077,1079,1082,1084,1086],{"class":91,"line":1078},35,[196,1080,1081],{"class":201},"            if",[196,1083,708],{"class":205},[196,1085,697],{"class":248},[196,1087,664],{"class":205},[196,1089,1091],{"class":91,"line":1090},36,[196,1092,1093],{"class":205},"                g.convertToMultiType()\n",[196,1095,1097],{"class":91,"line":1096},37,[196,1098,1099],{"class":205},"            f.setGeometry(g)\n",[196,1101,1103],{"class":91,"line":1102},38,[196,1104,1105],{"class":205},"        feats.append(f)\n",[196,1107,1109],{"class":91,"line":1108},39,[196,1110,1111],{"class":205},"    layer.dataProvider().addFeatures(feats)\n",[196,1113,1115],{"class":91,"line":1114},40,[196,1116,1117],{"class":205},"    layer.updateExtents()\n",[196,1119,1121,1123],{"class":91,"line":1120},41,[196,1122,639],{"class":201},[196,1124,1125],{"class":205}," layer\n",[196,1127,1129],{"class":91,"line":1128},42,[196,1130,233],{"emptyLinePlaceholder":232},[196,1132,1134,1137,1139,1142,1145],{"class":91,"line":1133},43,[196,1135,1136],{"class":205},"mem ",[196,1138,242],{"class":201},[196,1140,1141],{"class":205}," gdf_to_memory_layer(gdf, ",[196,1143,1144],{"class":248},"\"site buffers (memory)\"",[196,1146,259],{"class":205},[196,1148,1150],{"class":91,"line":1149},44,[196,1151,1152],{"class":205},"QgsProject.instance().addMapLayer(mem)\n",[14,1154,1155,1157,1158,1162,1163,1166,1167,1170,1171,1174,1175,330,1178,330,1181,1184,1185,1187,1188,1191,1192,1195],{},[433,1156,435],{}," The memory URI declares geometry type, CRS and every field in one string — by EPSG code where the frame's CRS has one, with a WKT fallback set on the layer for custom systems — as described in ",[21,1159,1161],{"href":1160},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fcreate-memory-layer-pyqgis\u002F","creating a memory layer",". Using ",[175,1164,1165],{},"int8"," (64-bit) for integers avoids overflow on large ids. If any geometry is multipart, the layer is declared as the multi type and every geometry is promoted, so a frame mixing ",[175,1168,1169],{},"Polygon"," and ",[175,1172,1173],{},"MultiPolygon"," loads without rejected features. Pandas missing values (",[175,1176,1177],{},"NaN",[175,1179,1180],{},"NaT",[175,1182,1183],{},"pd.NA",") become ",[175,1186,661],{},", which QGIS stores as NULL, and pandas timestamps become Python datetimes. ",[175,1189,1190],{},"geom.wkb"," is shapely's binary encoding, read by ",[175,1193,1194],{},"QgsGeometry.fromWkb"," without loss.",[160,1197,1199],{"id":1198},"handle-mixed-geometry-types","Handle mixed geometry types",[14,1201,1202],{},"A GeoDataFrame can hold points, lines and polygons in one column; a QGIS layer cannot. The frame must be split, or the geometry homogenised, before loading.",[187,1204,1206],{"className":189,"code":1205,"language":191,"meta":192,"style":192},"for gtype, part in gdf.groupby(gdf.geom_type.str.replace(\"Multi\", \"\")):\n    lyr = gdf_to_memory_layer(part, f\"result ({gtype})\", geom_type=gtype)\n    QgsProject.instance().addMapLayer(lyr)\n    print(gtype, lyr.featureCount())\n",[175,1207,1208,1230,1264,1269],{"__ignoreMap":192},[196,1209,1210,1212,1215,1217,1220,1222,1224,1227],{"class":91,"line":198},[196,1211,743],{"class":201},[196,1213,1214],{"class":205}," gtype, part ",[196,1216,749],{"class":201},[196,1218,1219],{"class":205}," gdf.groupby(gdf.geom_type.str.replace(",[196,1221,697],{"class":248},[196,1223,330],{"class":205},[196,1225,1226],{"class":248},"\"\"",[196,1228,1229],{"class":205},")):\n",[196,1231,1232,1235,1237,1240,1242,1245,1247,1249,1251,1254,1256,1259,1261],{"class":91,"line":215},[196,1233,1234],{"class":205},"    lyr ",[196,1236,242],{"class":201},[196,1238,1239],{"class":205}," gdf_to_memory_layer(part, ",[196,1241,359],{"class":201},[196,1243,1244],{"class":248},"\"result (",[196,1246,365],{"class":255},[196,1248,840],{"class":205},[196,1250,370],{"class":255},[196,1252,1253],{"class":248},")\"",[196,1255,330],{"class":205},[196,1257,1258],{"class":321},"geom_type",[196,1260,242],{"class":201},[196,1262,1263],{"class":205},"gtype)\n",[196,1265,1266],{"class":91,"line":229},[196,1267,1268],{"class":205},"    QgsProject.instance().addMapLayer(lyr)\n",[196,1270,1271,1274],{"class":91,"line":236},[196,1272,1273],{"class":255},"    print",[196,1275,1276],{"class":205},"(gtype, lyr.featureCount())\n",[14,1278,1279,1281,1282,1170,1284,1286,1287,1290],{},[433,1280,435],{}," Grouping by geometry family — with the \"Multi\" prefix stripped so ",[175,1283,1169],{},[175,1285,1173],{}," stay together — produces one layer per family. Mixed results are common after overlays in GeoPandas: an intersection of polygons can produce lines and points where shapes merely touch. Often those degenerate pieces should be dropped rather than loaded; filter with ",[175,1288,1289],{},"gdf[gdf.geom_type.isin([\"Polygon\", \"MultiPolygon\"])]"," when only areas matter. Writing mixed geometry to GeoPackage fails for the same reason, so the split is needed on both routes.",[160,1292,1294],{"id":1293},"keep-data-and-crs-consistent","Keep data and CRS consistent",[14,1296,1297],{},"Two subtle problems survive a successful load: coordinates in a different CRS from the one declared, and attribute values silently converted.",[14,1299,1300],{},[29,1301,1304,1307,1310,1313,1316,1320,1325,1329,1333,1336,1339,1342,1345,1350,1354,1357],{"viewBox":1302,"role":32,"ariaLabel":1303,"xmlns":34},"0 0 760 212","Three checks after loading a frame into QGIS: feature count, total bounds and sampled attribute values",[36,1305,1306],{},"Three checks after loading",[40,1308,1309],{},"After loading, compare three things between the frame and the layer: feature count, which catches rejected features; total bounds, which catch a CRS mismatch because coordinates land in the wrong place; and a sample of attribute values, which catches type conversion such as large integers truncated or dates turned into strings.",[44,1311],{"x":46,"y":46,"width":47,"height":1312,"fill":49},"212",[68,1314,1315],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Count, bounds, values",[44,1317],{"x":104,"y":1318,"width":1319,"height":530,"rx":58,"fill":106,"stroke":107,"style":84},"56","222",[68,1321,1324],{"x":1322,"y":1323,"style":88,"fill":107,"textAnchor":74},"135","103.78","feature count",[68,1326,1328],{"x":1322,"y":1327,"style":121,"fill":66,"textAnchor":74},"129.78","len(gdf) = layer",[68,1330,1332],{"x":1322,"y":1331,"style":121,"fill":83,"textAnchor":74},"155.78","catches rejects",[44,1334],{"x":1335,"y":1318,"width":1319,"height":530,"rx":58,"fill":126,"stroke":127,"style":84},"269",[68,1337,1338],{"x":70,"y":1323,"style":88,"fill":127,"textAnchor":74},"total bounds",[68,1340,1341],{"x":70,"y":1327,"style":121,"fill":66,"textAnchor":74},"same extent",[68,1343,1344],{"x":70,"y":1331,"style":121,"fill":83,"textAnchor":74},"catches CRS mix-ups",[44,1346],{"x":1347,"y":1318,"width":1319,"height":530,"rx":58,"fill":1348,"stroke":1349,"style":84},"514","#fdf2e2","#b45309",[68,1351,1353],{"x":1352,"y":1323,"style":88,"fill":1349,"textAnchor":74},"625","sample values",[68,1355,1356],{"x":1352,"y":1327,"style":121,"fill":66,"textAnchor":74},"spot-check rows",[68,1358,1359],{"x":1352,"y":1331,"style":121,"fill":83,"textAnchor":74},"catches type casts",[187,1361,1363],{"className":189,"code":1362,"language":191,"meta":192,"style":192},"def verify(gdf, layer, key):\n    assert layer.featureCount() == len(gdf), \"feature count differs\"\n    ext = layer.extent()\n    gx = gdf.total_bounds\n    assert abs(ext.xMinimum() - gx[0]) \u003C 1e-6 * max(1, abs(gx[0])), \"extent differs\"\n    for _, row in gdf.sample(min(5, len(gdf)), random_state=1).iterrows():\n        f = next(layer.getFeatures(f\"\\\"{key}\\\" = '{row[key]}'\"))\n        for col in gdf.columns.drop(gdf.geometry.name):\n            a, b = row[col], f[col]\n            if pd.isna(a):\n                continue\n            assert str(a)[:19] == str(b)[:19] or a == b, f\"{col}: {a!r} vs {b!r}\"\n    print(\"verified\")\n\nverify(gdf, mem, \"site_id\")\n",[175,1364,1365,1375,1395,1405,1415,1468,1505,1546,1559,1569,1576,1581,1656,1667,1671],{"__ignoreMap":192},[196,1366,1367,1369,1372],{"class":91,"line":198},[196,1368,569],{"class":201},[196,1370,1371],{"class":572}," verify",[196,1373,1374],{"class":205},"(gdf, layer, key):\n",[196,1376,1377,1380,1383,1386,1389,1392],{"class":91,"line":215},[196,1378,1379],{"class":201},"    assert",[196,1381,1382],{"class":205}," layer.featureCount() ",[196,1384,1385],{"class":201},"==",[196,1387,1388],{"class":255}," len",[196,1390,1391],{"class":205},"(gdf), ",[196,1393,1394],{"class":248},"\"feature count differs\"\n",[196,1396,1397,1400,1402],{"class":91,"line":229},[196,1398,1399],{"class":205},"    ext ",[196,1401,242],{"class":201},[196,1403,1404],{"class":205}," layer.extent()\n",[196,1406,1407,1410,1412],{"class":91,"line":236},[196,1408,1409],{"class":205},"    gx ",[196,1411,242],{"class":201},[196,1413,1414],{"class":205}," gdf.total_bounds\n",[196,1416,1417,1419,1422,1425,1428,1431,1433,1436,1439,1442,1445,1448,1450,1452,1454,1457,1460,1462,1465],{"class":91,"line":262},[196,1418,1379],{"class":201},[196,1420,1421],{"class":255}," abs",[196,1423,1424],{"class":205},"(ext.xMinimum() ",[196,1426,1427],{"class":201},"-",[196,1429,1430],{"class":205}," gx[",[196,1432,46],{"class":255},[196,1434,1435],{"class":205},"]) ",[196,1437,1438],{"class":201},"\u003C",[196,1440,1441],{"class":255}," 1e-6",[196,1443,1444],{"class":201}," *",[196,1446,1447],{"class":255}," max",[196,1449,409],{"class":205},[196,1451,896],{"class":255},[196,1453,330],{"class":205},[196,1455,1456],{"class":255},"abs",[196,1458,1459],{"class":205},"(gx[",[196,1461,46],{"class":255},[196,1463,1464],{"class":205},"])), ",[196,1466,1467],{"class":248},"\"extent differs\"\n",[196,1469,1470,1472,1475,1477,1480,1483,1485,1487,1489,1492,1495,1498,1500,1502],{"class":91,"line":279},[196,1471,955],{"class":201},[196,1473,1474],{"class":205}," _, row ",[196,1476,749],{"class":201},[196,1478,1479],{"class":205}," gdf.sample(",[196,1481,1482],{"class":255},"min",[196,1484,409],{"class":205},[196,1486,59],{"class":255},[196,1488,330],{"class":205},[196,1490,1491],{"class":255},"len",[196,1493,1494],{"class":205},"(gdf)), ",[196,1496,1497],{"class":321},"random_state",[196,1499,242],{"class":201},[196,1501,896],{"class":255},[196,1503,1504],{"class":205},").iterrows():\n",[196,1506,1507,1509,1511,1514,1517,1519,1521,1524,1527,1530,1533,1535,1538,1540,1543],{"class":91,"line":299},[196,1508,983],{"class":205},[196,1510,242],{"class":201},[196,1512,1513],{"class":255}," next",[196,1515,1516],{"class":205},"(layer.getFeatures(",[196,1518,359],{"class":201},[196,1520,362],{"class":248},[196,1522,1523],{"class":255},"\\\"{",[196,1525,1526],{"class":205},"key",[196,1528,1529],{"class":255},"}\\\"",[196,1531,1532],{"class":248}," = '",[196,1534,365],{"class":255},[196,1536,1537],{"class":205},"row[key]",[196,1539,370],{"class":255},[196,1541,1542],{"class":248},"'\"",[196,1544,1545],{"class":205},"))\n",[196,1547,1548,1551,1554,1556],{"class":91,"line":304},[196,1549,1550],{"class":201},"        for",[196,1552,1553],{"class":205}," col ",[196,1555,749],{"class":201},[196,1557,1558],{"class":205}," gdf.columns.drop(gdf.geometry.name):\n",[196,1560,1561,1564,1566],{"class":91,"line":315},[196,1562,1563],{"class":205},"            a, b ",[196,1565,242],{"class":201},[196,1567,1568],{"class":205}," row[col], f[col]\n",[196,1570,1571,1573],{"class":91,"line":343},[196,1572,1081],{"class":201},[196,1574,1575],{"class":205}," pd.isna(a):\n",[196,1577,1578],{"class":91,"line":348},[196,1579,1580],{"class":201},"                continue\n",[196,1582,1583,1586,1589,1592,1595,1597,1599,1601,1604,1606,1608,1610,1613,1615,1618,1620,1622,1624,1627,1629,1632,1634,1636,1639,1641,1644,1646,1649,1651,1653],{"class":91,"line":388},[196,1584,1585],{"class":201},"            assert",[196,1587,1588],{"class":255}," str",[196,1590,1591],{"class":205},"(a)[:",[196,1593,1594],{"class":255},"19",[196,1596,271],{"class":205},[196,1598,1385],{"class":201},[196,1600,1588],{"class":255},[196,1602,1603],{"class":205},"(b)[:",[196,1605,1594],{"class":255},[196,1607,271],{"class":205},[196,1609,678],{"class":201},[196,1611,1612],{"class":205}," a ",[196,1614,1385],{"class":201},[196,1616,1617],{"class":205}," b, ",[196,1619,359],{"class":201},[196,1621,362],{"class":248},[196,1623,365],{"class":255},[196,1625,1626],{"class":205},"col",[196,1628,370],{"class":255},[196,1630,1631],{"class":248},": ",[196,1633,365],{"class":255},[196,1635,21],{"class":205},[196,1637,1638],{"class":201},"!r",[196,1640,370],{"class":255},[196,1642,1643],{"class":248}," vs ",[196,1645,365],{"class":255},[196,1647,1648],{"class":205},"b",[196,1650,1638],{"class":201},[196,1652,370],{"class":255},[196,1654,1655],{"class":248},"\"\n",[196,1657,1658,1660,1662,1665],{"class":91,"line":400},[196,1659,1273],{"class":255},[196,1661,409],{"class":205},[196,1663,1664],{"class":248},"\"verified\"",[196,1666,259],{"class":205},[196,1668,1669],{"class":91,"line":417},[196,1670,233],{"emptyLinePlaceholder":232},[196,1672,1673,1676,1679],{"class":91,"line":423},[196,1674,1675],{"class":205},"verify(gdf, mem, ",[196,1677,1678],{"class":248},"\"site_id\"",[196,1680,259],{"class":205},[14,1682,1683,1685,1686,1689],{},[433,1684,435],{}," Feature count catches features rejected for geometry type. Comparing extents catches the classic mistake of reprojecting the frame but declaring the old CRS — the layer then appears hundreds of kilometres away or not at all. Sampling a few rows by key compares values loosely enough to tolerate harmless representation differences (a pandas timestamp versus a ",[175,1687,1688],{},"QDateTime"," printing slightly differently) while catching real changes. Running a check like this in scripts that hand results to other people is cheap insurance.",[160,1691,1693],{"id":1692},"write-several-results-into-one-geopackage","Write several results into one GeoPackage",[14,1695,1696],{},"An analysis usually produces more than one frame: the main result, intermediate layers worth keeping, a summary table without geometry. Writing them as tables in one GeoPackage and adding them to the project as a group keeps the output tidy and self-contained.",[187,1698,1700],{"className":189,"code":1699,"language":191,"meta":192,"style":192},"from qgis.core import QgsLayerTreeGroup\n\nresults = {\"site_buffers\": gdf, \"site_summary\": summary_gdf, \"flagged_sites\": flagged}\npath = \"\u002Fdata\u002Fresults\u002Fsite_analysis_2026-10.gpkg\"\nfor table, frame in results.items():\n    if \"geometry\" in frame.columns:\n        frame.to_file(path, layer=table, driver=\"GPKG\")\n    else:\n        gpd.GeoDataFrame(frame).to_file(path, layer=table, driver=\"GPKG\")\n\nroot = QgsProject.instance().layerTreeRoot()\ngroup = root.insertGroup(0, \"Site analysis 2026-10\")\nfor table in results:\n    lyr = QgsVectorLayer(f\"{path}|layername={table}\", table, \"ogr\")\n    QgsProject.instance().addMapLayer(lyr, addToLegend=False)\n    group.addLayer(lyr)\n",[175,1701,1702,1713,1717,1744,1753,1765,1778,1798,1806,1825,1829,1839,1858,1870,1906,1920],{"__ignoreMap":192},[196,1703,1704,1706,1708,1710],{"class":91,"line":198},[196,1705,218],{"class":201},[196,1707,221],{"class":205},[196,1709,202],{"class":201},[196,1711,1712],{"class":205}," QgsLayerTreeGroup\n",[196,1714,1715],{"class":91,"line":215},[196,1716,233],{"emptyLinePlaceholder":232},[196,1718,1719,1722,1724,1727,1729,1732,1735,1738,1741],{"class":91,"line":229},[196,1720,1721],{"class":205},"results ",[196,1723,242],{"class":201},[196,1725,1726],{"class":205}," {",[196,1728,327],{"class":248},[196,1730,1731],{"class":205},": gdf, ",[196,1733,1734],{"class":248},"\"site_summary\"",[196,1736,1737],{"class":205},": summary_gdf, ",[196,1739,1740],{"class":248},"\"flagged_sites\"",[196,1742,1743],{"class":205},": flagged}\n",[196,1745,1746,1748,1750],{"class":91,"line":236},[196,1747,307],{"class":205},[196,1749,242],{"class":201},[196,1751,1752],{"class":248}," \"\u002Fdata\u002Fresults\u002Fsite_analysis_2026-10.gpkg\"\n",[196,1754,1755,1757,1760,1762],{"class":91,"line":262},[196,1756,743],{"class":201},[196,1758,1759],{"class":205}," table, frame ",[196,1761,749],{"class":201},[196,1763,1764],{"class":205}," results.items():\n",[196,1766,1767,1769,1772,1775],{"class":91,"line":279},[196,1768,581],{"class":201},[196,1770,1771],{"class":248}," \"geometry\"",[196,1773,1774],{"class":201}," in",[196,1776,1777],{"class":205}," frame.columns:\n",[196,1779,1780,1783,1785,1787,1790,1792,1794,1796],{"class":91,"line":299},[196,1781,1782],{"class":205},"        frame.to_file(path, ",[196,1784,322],{"class":321},[196,1786,242],{"class":201},[196,1788,1789],{"class":205},"table, ",[196,1791,333],{"class":321},[196,1793,242],{"class":201},[196,1795,338],{"class":248},[196,1797,259],{"class":205},[196,1799,1800,1803],{"class":91,"line":304},[196,1801,1802],{"class":201},"    else",[196,1804,1805],{"class":205},":\n",[196,1807,1808,1811,1813,1815,1817,1819,1821,1823],{"class":91,"line":315},[196,1809,1810],{"class":205},"        gpd.GeoDataFrame(frame).to_file(path, ",[196,1812,322],{"class":321},[196,1814,242],{"class":201},[196,1816,1789],{"class":205},[196,1818,333],{"class":321},[196,1820,242],{"class":201},[196,1822,338],{"class":248},[196,1824,259],{"class":205},[196,1826,1827],{"class":91,"line":343},[196,1828,233],{"emptyLinePlaceholder":232},[196,1830,1831,1834,1836],{"class":91,"line":348},[196,1832,1833],{"class":205},"root ",[196,1835,242],{"class":201},[196,1837,1838],{"class":205}," QgsProject.instance().layerTreeRoot()\n",[196,1840,1841,1844,1846,1849,1851,1853,1856],{"class":91,"line":388},[196,1842,1843],{"class":205},"group ",[196,1845,242],{"class":201},[196,1847,1848],{"class":205}," root.insertGroup(",[196,1850,46],{"class":255},[196,1852,330],{"class":205},[196,1854,1855],{"class":248},"\"Site analysis 2026-10\"",[196,1857,259],{"class":205},[196,1859,1860,1862,1865,1867],{"class":91,"line":400},[196,1861,743],{"class":201},[196,1863,1864],{"class":205}," table ",[196,1866,749],{"class":201},[196,1868,1869],{"class":205}," results:\n",[196,1871,1872,1874,1876,1878,1880,1882,1884,1886,1888,1890,1892,1895,1897,1899,1902,1904],{"class":91,"line":417},[196,1873,1234],{"class":205},[196,1875,242],{"class":201},[196,1877,356],{"class":205},[196,1879,359],{"class":201},[196,1881,362],{"class":248},[196,1883,365],{"class":255},[196,1885,63],{"class":205},[196,1887,370],{"class":255},[196,1889,443],{"class":248},[196,1891,365],{"class":255},[196,1893,1894],{"class":205},"table",[196,1896,370],{"class":255},[196,1898,362],{"class":248},[196,1900,1901],{"class":205},", table, ",[196,1903,383],{"class":248},[196,1905,259],{"class":205},[196,1907,1908,1911,1914,1916,1918],{"class":91,"line":423},[196,1909,1910],{"class":205},"    QgsProject.instance().addMapLayer(lyr, ",[196,1912,1913],{"class":321},"addToLegend",[196,1915,242],{"class":201},[196,1917,974],{"class":255},[196,1919,259],{"class":205},[196,1921,1922],{"class":91,"line":667},[196,1923,1924],{"class":205},"    group.addLayer(lyr)\n",[14,1926,1927,1929,1930,1932,1933,1935,1936,1939,1940,1944],{},[433,1928,435],{}," Each ",[175,1931,439],{}," call with a different ",[175,1934,322],{}," adds a table to the same GeoPackage. A frame without a geometry column is written as an attribute-only table, which QGIS opens as a plain table you can join to the spatial layers. Adding layers with ",[175,1937,1938],{},"addToLegend=False"," and then placing them in a named group keeps them together at the top of the Layers panel, as described in ",[21,1941,1943],{"href":1942},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis\u002F","organising layer tree groups",". Naming the file and group after the run date makes successive analyses easy to tell apart.",[160,1946,1948],{"id":1947},"style-the-new-layer","Style the new layer",[14,1950,1951],{},"A newly loaded layer gets a random colour. A couple of lines give it a style that carries meaning — here a graduated renderer on a numeric result column.",[187,1953,1955],{"className":189,"code":1954,"language":191,"meta":192,"style":192},"from qgis.core import QgsGraduatedSymbolRenderer, QgsStyle, QgsClassificationJenks\n\nrenderer = QgsGraduatedSymbolRenderer(\"score\")\nrenderer.setClassificationMethod(QgsClassificationJenks())\nrenderer.updateClasses(mem, 5)\nrenderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp(\"Viridis\"))\nmem.setRenderer(renderer)\nmem.triggerRepaint()\n",[175,1956,1957,1968,1972,1987,1992,2001,2011,2016],{"__ignoreMap":192},[196,1958,1959,1961,1963,1965],{"class":91,"line":198},[196,1960,218],{"class":201},[196,1962,221],{"class":205},[196,1964,202],{"class":201},[196,1966,1967],{"class":205}," QgsGraduatedSymbolRenderer, QgsStyle, QgsClassificationJenks\n",[196,1969,1970],{"class":91,"line":215},[196,1971,233],{"emptyLinePlaceholder":232},[196,1973,1974,1977,1979,1982,1985],{"class":91,"line":229},[196,1975,1976],{"class":205},"renderer ",[196,1978,242],{"class":201},[196,1980,1981],{"class":205}," QgsGraduatedSymbolRenderer(",[196,1983,1984],{"class":248},"\"score\"",[196,1986,259],{"class":205},[196,1988,1989],{"class":91,"line":236},[196,1990,1991],{"class":205},"renderer.setClassificationMethod(QgsClassificationJenks())\n",[196,1993,1994,1997,1999],{"class":91,"line":262},[196,1995,1996],{"class":205},"renderer.updateClasses(mem, ",[196,1998,59],{"class":255},[196,2000,259],{"class":205},[196,2002,2003,2006,2009],{"class":91,"line":279},[196,2004,2005],{"class":205},"renderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp(",[196,2007,2008],{"class":248},"\"Viridis\"",[196,2010,1545],{"class":205},[196,2012,2013],{"class":91,"line":299},[196,2014,2015],{"class":205},"mem.setRenderer(renderer)\n",[196,2017,2018],{"class":91,"line":304},[196,2019,2020],{"class":205},"mem.triggerRepaint()\n",[14,2022,2023,2025,2026,2030,2031,2035],{},[433,2024,435],{}," Natural breaks with five classes and a perceptually uniform ramp is a sensible default for a continuous result; ",[21,2027,2029],{"href":2028},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002F","classifying with natural breaks"," explains the options. If a style already exists for this kind of output, load it from a QML file instead, as in ",[21,2032,2034],{"href":2033},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fsave-and-load-qml-style-pyqgis\u002F","saving and loading QML styles",", so every run looks the same.",[160,2037,2039],{"id":2038},"qgis-version-compatibility","QGIS version compatibility",[14,2041,2042,2043,2045,2046,2049,2050,2052,2053,2055,2056,2059],{},"Both routes work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The memory provider's ",[175,2044,1165],{}," type needs 3.28 or newer; use ",[175,2047,2048],{},"integer"," on older releases. GeoPandas 0.14 and later, with shapely 2, are assumed. On QGIS 4, setting ",[175,2051,661],{}," as an attribute is the native NULL; on QGIS 3, ",[175,2054,661],{}," is also accepted by ",[175,2057,2058],{},"setAttributes"," and stored as NULL.",[160,2061,2063],{"id":2062},"troubleshooting","Troubleshooting",[165,2065,2066,2072,2085,2095],{},[168,2067,2068,2071],{},[433,2069,2070],{},"Some features are missing."," Mixed geometry types; split the frame by family or filter degenerate results.",[168,2073,2074,2077,2078,2080,2081,2084],{},[433,2075,2076],{},"The layer appears in the wrong place."," The CRS declared does not match the coordinates; check ",[175,2079,177],{}," after every ",[175,2082,2083],{},"to_crs",".",[168,2086,2087,2090,2091,2094],{},[433,2088,2089],{},"Integer columns became doubles."," The column had missing values, so pandas made it float; use ",[175,2092,2093],{},"astype(\"Int64\")"," before loading.",[168,2096,2097,2102],{},[433,2098,2099,2101],{},[175,2100,439],{}," fails with a field name error."," GeoPackage column names are limited; rename long or duplicate names.",[160,2104,2106],{"id":2105},"conclusion","Conclusion",[14,2108,2109],{},"Write to GeoPackage and load the file for anything persistent or large; build a memory layer from dtypes and WKB for session-only results; split mixed geometry types by family; convert pandas missing values and timestamps; verify count, extent and sample values; and apply a meaningful style straight away.",[160,2111,2113],{"id":2112},"frequently-asked-questions","Frequently Asked Questions",[14,2115,2116,2119,2120,1170,2123,2126],{},[433,2117,2118],{},"Can I update an existing QGIS layer from a GeoDataFrame?","\nYes: match rows by key and use ",[175,2121,2122],{},"changeAttributeValues",[175,2124,2125],{},"changeGeometryValues"," on the provider, or within an edit session.",[14,2128,2129,2132],{},[433,2130,2131],{},"Is the memory route faster?","\nFor small and medium frames, yes. For large frames, GeoPackage writing in compiled code often wins.",[14,2134,2135,2138],{},[433,2136,2137],{},"Does styling survive if I re-run the script?","\nNot for memory layers. Save a QML and apply it after loading.",[14,2140,2141,2144,2145,2147],{},[433,2142,2143],{},"Can I load a plain DataFrame without geometry?","\nYes, as a ",[175,2146,661],{}," geometry memory layer — useful for joining to existing layers.",[160,2149,2151],{"id":2150},"related","Related",[165,2153,2154,2159,2165,2170,2176],{},[168,2155,2156,2158],{},[21,2157,24],{"href":23}," — the guide this recipe belongs to",[168,2160,2161],{},[21,2162,2164],{"href":2163},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-layer-to-geopandas-geodataframe-pyqgis\u002F","Convert a QGIS Layer to a GeoPandas GeoDataFrame",[168,2166,2167],{},[21,2168,2169],{"href":1160},"Create a Memory Layer in PyQGIS",[168,2171,2172],{},[21,2173,2175],{"href":2174},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fwrite-vector-layer-to-geopackage-pyqgis\u002F","Write a Vector Layer to GeoPackage in PyQGIS",[168,2177,2178],{},[21,2179,2181],{"href":2180},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-between-qgsgeometry-and-shapely\u002F","Convert Between QgsGeometry and Shapely",[2183,2184,2185],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":192,"searchDepth":215,"depth":215,"links":2187},[2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199],{"id":162,"depth":215,"text":163},{"id":181,"depth":215,"text":182},{"id":447,"depth":215,"text":448},{"id":1198,"depth":215,"text":1199},{"id":1293,"depth":215,"text":1294},{"id":1692,"depth":215,"text":1693},{"id":1947,"depth":215,"text":1948},{"id":2038,"depth":215,"text":2039},{"id":2062,"depth":215,"text":2063},{"id":2105,"depth":215,"text":2106},{"id":2112,"depth":215,"text":2113},{"id":2150,"depth":215,"text":2151},"Put GeoPandas results on the QGIS map — writing to GeoPackage and loading the file, or building a memory layer with matching field types directly from the frame — with CRS, mixed geometry types, NULLs, dates and styling handled correctly.","md",{"slug":2203,"type":2204,"breadcrumb":5,"datePublished":2205,"dateModified":2205},"load-geodataframe-into-qgis-pyqgis","article","2026-10-02","\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fload-geodataframe-into-qgis-pyqgis",{"title":5,"description":2200},"spatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fload-geodataframe-into-qgis-pyqgis\u002Findex","HunN17kM6oyH3HtJTsfE-vd8C2EAf61KmttkuKQep94",1790966264375]