如何在Pandas系列中找到最接近输入数字的值?

106

3
请注意题目要求的是最接近的值,但问题本身则询问最接近的上下界。从某个答案的评论中可以看到,它们并不相同(这里的大多数答案似乎回答的是标题,而非问题本身)。 - 9769953
9个回答

99
你可以使用argsort(),例如:input = 3
In [198]: input = 3

In [199]: df.iloc[(df['num']-input).abs().argsort()[:2]]
Out[199]:
   num
2    4
4    2

df_sort 是包含两个最接近数值的数据框。

In [200]: df_sort = df.iloc[(df['num']-input).abs().argsort()[:2]]

对于索引,

In [201]: df_sort.index.tolist()
Out[201]: [2, 4]

对于数值,

In [202]: df_sort['num'].tolist()
Out[202]: [4, 2]

对于上述解决方案,df 是关键。

In [197]: df
Out[197]:
   num
0    1
1    6
2    4
3    5
4    2

3
这个功能会找到最接近的上下两个,还是只会找到两个最接近的? - Steve
下面和上面是什么意思?最接近的值是通过它们与给定输入的绝对差来选择的。 - Zero
3
我需要找到 a) 最接近的上限数字,b) 最接近的下限数字。因此,仅使用绝对差可能无法在所有情况下实现这一点。 - Steve
3
这会给出错误的答案。我在一个更复杂的数据集上尝试了这个方法。你必须使用.iloc而不是.ix,这样就有效了(见@op1)。 - amc
1
这种方法的复杂度不是N log(N)吗,因为你需要排序吗?这似乎并不是非常高效的,因为应该可以在 ~O(N) 的时间内执行搜索? - Ivo Merchiers
显示剩余2条评论

49

除了不能完全回答问题之外,其他在这里讨论的算法的额外缺点是它们必须对整个列表进行排序。这导致了复杂度为 ~N log(N)

然而,可以通过以下方法在 ~N 的时间内获得相同结果。该方法将数据框分成两个子集,一个小于所需值,另一个大于所需值。较小数据帧中的最大值是下限相邻值,而较大数据帧中的最小值是上限相邻值。

这给出以下代码片段:

def find_neighbours(value, df, colname):
    exactmatch = df[df[colname] == value]
    if not exactmatch.empty:
        return exactmatch.index
    else:
        lowerneighbour_ind = df[df[colname] < value][colname].idxmax()
        upperneighbour_ind = df[df[colname] > value][colname].idxmin()
        return [lowerneighbour_ind, upperneighbour_ind] 

这种方法类似于使用 pandas中的partition,在处理大型数据集并且遇到复杂性问题时非常有用。


比较这两种策略可以发现,对于大的N,分区策略确实更快。对于小的N,排序策略将更有效,因为它是在更低的级别上实现的。它还是一行代码,可能增加了代码的可读性。 Partition和Sort的比较

复制此图表的代码如下:

from matplotlib import pyplot as plt
import pandas
import numpy
import timeit

value=3
sizes=numpy.logspace(2, 5, num=50, dtype=int)

sort_results, partition_results=[],[]
for size in sizes:
    df=pandas.DataFrame({"num":100*numpy.random.random(size)})
    
    sort_results.append(timeit.Timer("df.iloc[(df['num']-value).abs().argsort()[:2]].index",
                                         globals={'find_neighbours':find_neighbours, 'df':df,'value':value}).autorange())
    partition_results.append(timeit.Timer('find_neighbours(df,value)',
                                          globals={'find_neighbours':find_neighbours, 'df':df,'value':value}).autorange())
    
sort_time=[time/amount for amount,time in sort_results]
partition_time=[time/amount for amount,time in partition_results]

plt.plot(sizes, sort_time)
plt.plot(sizes, partition_time)
plt.legend(['Sorting','Partitioning'])
plt.title('Comparison of strategies')
plt.xlabel('Size of Dataframe')
plt.ylabel('Time in s')
plt.savefig('speed_comparison.png')

1
我喜欢这个想法!然而,如果索引不是单调的,在我的看来lower..._idupper..._id很有可能不会相邻;这也是其他算法通过排序提供的。 - Joël
1
@Joël,我觉得我不理解你的意思。它们必须彼此相邻,因为lower...是小于value的最大值,而upper...是大于lower的最小值。我不明白还有什么其他值可以在它们之间?或者你指的是其他的东西? - Ivo Merchiers
1
我认为这个答案更正确,因为它提供了最接近的下界和上界,而不仅仅是两个最接近的值。它有两个(小)错误:第3行缩进和使用未知的“traversed”变量(可能应该是value); 因为我对错误大约90%确定,而不是100%,所以我还有点犹豫是否编辑和修复答案。 - 9769953
2
@ClaudiuCreanga 这取决于 df 的大小。我添加了一个图表和一些代码来说明这种行为。 - Ivo Merchiers
1
@Isaac 感谢您的建议,我已经批准了这个编辑。尽管评论对理解您的编辑很有帮助。 - Ivo Merchiers
显示剩余6条评论

23

我建议除了John Galt的答案之外,还要使用iloc,因为这将即使在未排序的整数索引下也能正常工作,因为.ix首先查看索引标签。

df.iloc[(df['num']-input).abs().argsort()[:2]]

1
ix已被弃用:http://pandas-docs.github.io/pandas-docs-travis/whatsnew.html#deprecate-ix - ecoe

11
如果序列已经排序,查找索引的高效方法是使用bisect函数。例如:
idx = bisect_left(df['num'].values, 3)

假设数据框df的列col已排序

  • 如果值val在该列中,bisect_left将返回列表中值的精确索引,bisect_right将返回下一个位置的索引。
  • 如果该值不在列表中,bisect_leftbisect_right将返回相同的索引:将该值插入到保持列表排序的位置。

因此,为了回答这个问题,以下代码在找到valcol中的索引时起作用,否则会返回最接近的值的索引。即使列表中的值不唯一,此解决方案也可以工作。

from bisect import bisect_left, bisect_right

def get_closests(df, col, val):
    lower_idx = bisect_left(df[col].values, val)
    higher_idx = bisect_right(df[col].values, val)
    if higher_idx == lower_idx:      #val is not in the list
        return lower_idx - 1, lower_idx
    else:                            #val is in the list
        return lower_idx

二分算法非常高效,可以找到数据帧列“col”中特定值“val”的索引,或其最接近的邻居,但需要对列表进行排序。

2
我找到的解决这类问题最直观的方法是使用@ivo-merchiers建议的分区方法,但使用nsmallest和nlargest。除了可以处理未排序的系列外,这种方法的一个好处是通过将k_matches设置为大于1的数字,您可以轻松地获取多个接近的值。
import pandas as pd
source = pd.Series([1,6,4,5,2])
target = 3

def find_closest_values(target, source, k_matches=1):
    k_above = source[source >= target].nsmallest(k_matches+1)
    k_below = source[source < target].nlargest(k_matches)
    k_all = pd.concat([k_below, k_above]).sort_values()
    return k_all

find_closest_values(target, source, k_matches=1)

输出:

4    2
2    4
dtype: int64

2
你可以使用numpy.searchsorted。如果你的搜索列没有排序,你可以创建一个已排序的DataFrame,并使用pandas.argsort记住它们之间的映射。(如果你计划多次查找最接近的值,这比上述方法更好。)一旦排序完成,可以像这样查找输入的最接近值:
indLeft = np.searchsorted(df['column'], input, side='left')
indRight = np.searchsorted(df['column'], input, side='right')

valLeft = df['column'][indLeft]
valRight = df['column'][indRight]

1
如果输入与列中的元素不完全匹配,则我认为indLeft和indRight相等。请求是获取最接近的两个索引。 - Michael Chen

2
如果您的系列已经排序,您可以使用以下类似的方法。
def closest(df, col, val, direction):
    n = len(df[df[col] <= val])
    if(direction < 0):
        n -= 1
    if(n < 0 or n >= len(df)):
        print('err - value outside range')
        return None
    return df.ix[n, col]    

df = pd.DataFrame(pd.Series(range(0,10,2)), columns=['num'])
for find in range(-1, 2):
    lc = closest(df, 'num', find, -1)
    hc = closest(df, 'num', find, 1)
    print('Closest to {} is {}, lower and {}, higher.'.format(find, lc, hc))


df:     num
    0   0
    1   2
    2   4
    3   6
    4   8
err - value outside range
Closest to -1 is None, lower and 0, higher.
Closest to 0 is 0, lower and 2, higher.
Closest to 1 is 0, lower and 2, higher.

0

如果您需要在“num”列中找到最接近obj_num的值,并且在有多个选择的情况下,可以根据其他列(例如第二列'num2')的值选择最佳出现次数。

为此,我建议创建一个新列'num_diff',然后使用sort_values。例如:我们想要选择在“num”列中最接近3的值,并且在有许多出现次数的情况下,在“num2”列上选择最小值。代码如下:

import pandas as pd

obj_num = 3
df = pd.DataFrame({
    'num': [0, 1, 3, 3, 3, 4],
    'num2': [0, 0, 0, -1, 1, 0]
})

df_copy = df.loc[:, ['num', 'num2']].copy()
df_copy['num_diff'] = (df['num']-obj_num).abs()
df_copy.sort_values(
    by=['num_diff', 'num2'],
    axis=0,
    inplace=True
)
obj_num_idx = df_copy.index[0]

print(f'Objective row: \n{df.loc[obj_num_idx, :]}')

这是一个使用目标值和列的字典来完成任务的函数(它尊重用于排序的列的顺序):
def colosest_row(df, obj):
    '''
    Sort df using specific columns given as obj keys.
    If a key has None value:
        sort column in ascending order.
    If a key has a float value:
        sort column from closest to farest value from obj[key] value.

    Arguments
    ---------
    df: pd.DataFrame
        contains at least obj keys in its columns.
    obj: dict
        dict of objective columns.
    
    Return
    ------
    index of closest row to obj
    '''
    df_copy = df.loc[:, [*obj]].copy()

    special_cols = []
    obj_cols = []
    for key in obj:
        if obj[key] is None:
            obj_cols.append(key)
        else:
            special_cols.append(key)
            obj_cols.append(f'{key}_diff')

    for key in special_cols:
        df_copy[f'{key}_diff'] = (df[key]-obj[key]).abs()

    df_copy.sort_values(
        by=obj_cols,
        axis=0,
        ascending=True,
        inplace=True
    )

    return df_copy.index[0]

obj_num_idx = colosest_row(
    df=df,
    obj={
        "num": obj_num,
        "num2": None  # Sort using also 'num2'
    }
)

-3

这里有很多答案,其中许多都非常好。没有一个被接受,@Zero的答案目前评分最高。另一个答案指出当索引未排序时它不起作用,但他/她推荐了一个看起来已经过时的解决方案。

我发现我可以使用numpy版本的argsort()在以下方式中对值本身进行排序,即使索引未排序也可以:

df.iloc[(df['num']-input).abs()..values.argsort()[:2]]

请参考Zero的回答以获取上下文。


我认为在这里使用argmin()会更好。 - zylatis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接