Tengo un df con números:
numbers = pd.DataFrame(columns=['number'], data=[ 50, 65, 75, 85, 90 ])y un df con rangos (ver tabla):
ranges = pd.DataFrame( columns=['range','range_min','range_max'], data=[ ['A',90,100], ['B',85,95], ['C',70,80] ] )Quiero determinar en qué rango (en la segunda tabla) cae un valor (en la primera tabla). Tenga en cuenta que los rangos se superponen y los límites son inclusivos. También tenga en cuenta que el marco de datos estándar anterior tiene 3 rangos; sin embargo, este marco de datos se genera dinámicamente. Puede tener de 2 a 7 rangos.
Resultado deseado:
numbers = pd.DataFrame(columns=['number','detected_range'], data=[ [50,'out_of_range'], [65, 'out_of_range'], [75,'C'], [85,'B'], [90,'overlap'] * could be A or B * ])Resolví esto con un bucle for pero esto no se adapta bien a un gran conjunto de datos que estoy usando. Además, el código es demasiado extenso y poco elegante. Vea abajo:
numbers['detected_range'] = nan for i, row1 in number.iterrows(): for j, row2 in ranges.iterrows(): if row1.number<row2.range_min and row1.number>row2.range_max: numbers.loc[i,'detected_range'] = row1.loc[j,'range'] else if (other cases...): ...and so on...¿Cómo podría hacer esto?
Puede usar un poco de operaciones vectoriales numpy para generar máscaras y usarlas para select sus etiquetas:
import numpy as np a = numbers['number'].values # numpy array of numbers r = ranges.set_index('range') # dataframe of min/max with labels as index m1 = (a>=r['range_min'].values[:,None]).T # is number above each min m2 = (a<r['range_max'].values[:,None]).T # is number below each max m3 = (m1&m2) # combine both conditions above # NB. the two operations could be done without the intermediate variables m1/m2 m4 = m3.sum(1) # how many matches? # 0 -> out_of_range # 2 -> overlap # 1 -> get column name # now we select the label according to the conditions numbers['detected_range'] = np.select([m4==0, m4==2], # out_of_range and overlap ['out_of_range', 'overlap'], # otherwise get column name default=np.take(r.index, m3.argmax(1)) )producción:
number detected_range 0 50 out_of_range 1 65 out_of_range 2 75 C 3 85 B 4 90 overlapFunciona con cualquier número de intervalos en rangos.
ejemplo de salida con extra ['D',50,51] :
number detected_range 0 50 D 1 65 out_of_range 2 75 C 3 85 B 4 90 overlapPandas IntervalIndex encaja aquí; sin embargo, dado que sus datos tienen puntos superpuestos, un bucle for es el enfoque que usaré aquí (para índices únicos que no se superponen, pd.get_indexer es un enfoque rápido):
intervals = pd.IntervalIndex.from_arrays(ranges.range_min, ranges.range_max, closed='both') box = [] for num in numbers.number: bools = intervals.contains(num) if bools.sum()==1: box.append(ranges.range[bools].item()) elif bools.sum() > 1: box.append('overlap') else: box.append('out_of_range') numbers.assign(detected_range = box) number detected_range 0 50 out_of_range 1 65 out_of_range 2 75 C 3 85 B 4 90 overlap