@@ -42,67 +42,106 @@ def count_max_consec_detects(nest_data, date_data):
4242
4343
4444def process_nests (nest_file , year , site , savedir , min_score = 0.3 , min_detections = 3 , min_consec_detects = 1 ):
45- """Process nests into a one row per nest table"""
45+ """Process nests into a one-row-per-nest table"""
46+ SCHEMA = {
47+ "geometry" : "Point" ,
48+ "properties" : {
49+ "nest_id" : "int" ,
50+ "Site" : "str" ,
51+ "Year" : "str" ,
52+ "xmean" : "float" ,
53+ "ymean" : "float" ,
54+ "first_obs" : "str" ,
55+ "last_obs" : "str" ,
56+ "num_obs" : "int" ,
57+ "species" : "str" ,
58+ "sum_top1" : "float" ,
59+ "num_top1" : "int" ,
60+ "bird_match" : "str" ,
61+ },
62+ }
63+
4664 nests_data = geopandas .read_file (nest_file )
65+
66+ # Convert numeric columns to correct types if they're strings
67+ # (shapefiles sometimes read them as strings)
68+ numeric_columns = ['score' , 'match_xmin' , 'match_xmax' , 'match_ymin' , 'match_ymax' , 'target_ind' , 'bird_id' ]
69+ # Only convert columns that exist and are object type
70+ cols_to_convert = [
71+ col for col in numeric_columns if col in nests_data .columns and nests_data [col ].dtype == 'object'
72+ ]
73+ if cols_to_convert :
74+ nests_data [cols_to_convert ] = nests_data [cols_to_convert ].apply (pd .to_numeric , errors = 'coerce' )
75+
76+ # Build date_data: single row with all dates for the site-year
4777 date_data = nests_data .groupby (['Site' , 'Year' ]).agg ({'Date' : lambda x : x .unique ().tolist ()}).reset_index ()
4878 target_inds = nests_data ['target_ind' ].unique ()
49- nests = []
79+ nests_rows = []
80+
5081 for target_ind in target_inds :
51- nest_data = nests_data [(nests_data [' target_ind' ] == target_ind ) & (nests_data [' score' ] >= min_score )]
82+ nest_data = nests_data [(nests_data [" target_ind" ] == target_ind ) & (nests_data [" score" ] >= min_score )]
5283 num_consec_detects = count_max_consec_detects (nest_data , date_data )
84+
5385 if len (nest_data ) >= min_detections or num_consec_detects >= min_consec_detects :
86+ # Aggregate scores per label and pick the top label by summed score
5487 summed_scores = nest_data .groupby (['Site' , 'Year' , 'target_ind' , 'label' ]).score .agg (['sum' , 'count' ])
5588 top_score_data = summed_scores [summed_scores ['sum' ] == max (summed_scores ['sum' ])].reset_index ()
89+
5690 nest_info = nest_data .groupby (['Site' , 'Year' , 'target_ind' ]).agg ({
5791 'Date' : ['min' , 'max' , 'count' ],
5892 'match_xmin' : ['mean' ],
5993 'match_ymin' : ['mean' ],
6094 'match_xmax' : ['mean' ],
6195 'match_ymax' : ['mean' ]
6296 }).reset_index ()
63- xmean = (nest_info ['match_xmin' ]['mean' ][0 ] + nest_info ['match_xmax' ]['mean' ]) / 2
64- ymean = (nest_info ['match_ymin' ]['mean' ][0 ] + nest_info ['match_ymax' ]['mean' ]) / 2
65- bird_match = "," .join ([str (x ) for x in nest_data ["bird_id" ]])
66- nests .append ([
67- target_ind , nest_info ['Site' ][0 ], nest_info ['Year' ][0 ], xmean [0 ], ymean [0 ], nest_info ['Date' ]['min' ][0 ],
68- nest_info ['Date' ]['max' ][0 ], nest_info ['Date' ]['count' ][0 ], top_score_data ['label' ][0 ],
69- top_score_data ['sum' ][0 ], top_score_data ['count' ][0 ], bird_match
97+
98+ xmean = (nest_info ['match_xmin' ]['mean' ][0 ] + nest_info ['match_xmax' ]['mean' ][0 ]) / 2
99+ ymean = (nest_info ['match_ymin' ]['mean' ][0 ] + nest_info ['match_ymax' ]['mean' ][0 ]) / 2
100+
101+ nests_rows .append ([
102+ int (target_ind ),
103+ str (nest_info ['Site' ][0 ]),
104+ str (nest_info ['Year' ][0 ]),
105+ float (xmean ),
106+ float (ymean ),
107+ str (nest_info ['Date' ]['min' ][0 ]),
108+ str (nest_info ['Date' ]['max' ][0 ]),
109+ int (nest_info ['Date' ]['count' ][0 ]),
110+ str (top_score_data ['label' ][0 ]),
111+ float (top_score_data ['sum' ][0 ]),
112+ int (top_score_data ['count' ][0 ]),
113+ "," .join (str (x ) for x in nest_data ["bird_id" ]),
70114 ])
71115
72- if not os .path .exists (savedir ):
73- os .makedirs (savedir )
116+ os .makedirs (savedir , exist_ok = True )
74117 filename = os .path .join (savedir , f"{ site } _{ year } _processed_nests.shp" )
75118
76- if nests :
77- nests = pd . DataFrame ( nests ,
78- columns = [
79- 'nest_id' , 'Site' , 'Year' , 'xmean' , 'ymean' , 'first_obs' , 'last_obs' , 'num_obs' ,
80- 'species' , 'sum_top1' , 'num_top1' , 'bird_match'
81- ])
82- nests_shp = geopandas . GeoDataFrame ( nests , geometry = geopandas . points_from_xy ( nests . xmean , nests . ymean ))
83- nests_shp . crs = nests_data . crs
84- nests_shp . to_file ( filename )
119+ gdf_tofile = None
120+ if nests_rows :
121+ nests_df = pd . DataFrame ( nests_rows , columns = list ( SCHEMA [ "properties" ]. keys ()))
122+ nests_gdf = geopandas . GeoDataFrame (
123+ nests_df ,
124+ geometry = geopandas . points_from_xy ( nests_df . xmean , nests_df . ymean ),
125+ crs = nests_data . crs ,
126+ )
127+ gdf_tofile = nests_gdf
85128 else :
86- schema = {
87- "geometry" : "Polygon" ,
88- "properties" : {
89- 'nest_id' : 'int' ,
90- 'Site' : 'str' ,
91- 'Year' : 'str' ,
92- 'xmean' : 'float' ,
93- 'ymean' : 'float' ,
94- 'first_obs' : 'str' ,
95- 'last_obs' : 'str' ,
96- 'num_obs' : 'int' ,
97- 'species' : 'str' ,
98- 'sum_top1' : 'float' ,
99- 'num_top1' : 'int' ,
100- 'bird_match' : 'str'
101- }
129+ empty_data = {
130+ k : pd .Series (dtype = "int64" if v == "int" else "float64" if v == "float" else "object" )
131+ for k , v in SCHEMA ["properties" ].items ()
102132 }
103- crs = nests_data .crs
104- empty_nests = geopandas .GeoDataFrame (geometry = [])
105- empty_nests .to_file (filename , driver = 'ESRI Shapefile' , schema = schema , crs = crs )
133+ empty_gdf = geopandas .GeoDataFrame (
134+ empty_data ,
135+ geometry = geopandas .GeoSeries ([], dtype = "geometry" ),
136+ crs = nests_data .crs ,
137+ )
138+ gdf_tofile = empty_gdf
139+
140+ try :
141+ import pyogrio
142+ gdf_tofile .to_file (filename , driver = "ESRI Shapefile" , engine = "pyogrio" )
143+ except ImportError :
144+ gdf_tofile .to_file (filename , driver = "ESRI Shapefile" , engine = "fiona" )
106145
107146
108147if __name__ == "__main__" :
0 commit comments