Skip to content

    Chapter 20, Intermediate

    Sort, search and count

    Sorting an array is easy. What is really used is the **position** in which each element would be, because it allows reordering other arrays in the same order.

    Code for this chapter: intermediario/cap20_ordenar_buscar.py

    `sort` and `argsort`

    The np.sort returns a sorted copy. The np.argsort returns, instead of the values, the positions that would sort the array, and this is the most versatile tool:

    intermediario/cap20_ordenar_buscar.pylinhas 10 a 15
    import numpy as np
    
    v = np.array([30, 10, 50, 20, 40])
    print(np.sort(v), v)
    print(np.argsort(v))
    print(v[np.argsort(v)[::-1]])
    
    Output
    [10 20 30 40 50] [30 10 50 20 40]
    [1 3 0 4 2]
    [50 40 30 20 10]
    

    With the positions you sort an array by the key of another: sorting names by grades, for example. For descending order, sort the negative:

    intermediario/cap20_ordenar_buscar.pylinhas 17 a 19
    nomes = np.array(["Ana", "Bia", "Caio"])
    notas = np.array([7.5, 9.0, 6.0])
    print(nomes[np.argsort(-notas)])
    
    Output
    ['Bia' 'Ana' 'Caio']
    

    In two dimensions, the axis chooses the direction of the sorting:

    intermediario/cap20_ordenar_buscar.pylinhas 21 a 23
    m = np.array([[3, 1, 2], [9, 8, 7]])
    print(np.sort(m, axis=1))
    print(np.sort(m, axis=0))
    
    Output
    [[1 2 3]
     [7 8 9]]
    [[3 1 2]
     [9 8 7]]
    

    Sort by multiple criteria

    The np.lexsort sorts by multiple keys, and the last key of the tuple is the main one. To sort by last name and, in case of a tie, by age, age comes first:

    intermediario/cap20_ordenar_buscar.pylinhas 28 a 31
    sobrenome = np.array(["Silva", "Souza", "Silva", "Souza"])
    idade = np.array([30, 25, 20, 40])
    ordem = np.lexsort((idade, sobrenome))
    print(ordem.tolist())
    
    Output
    [2, 0, 1, 3]
    

    Search in a sorted array

    The np.searchsorted finds, by binary search (fast even with millions of elements), the position where each value would fit. It is the basis for grouping values into ranges, and the np.digitize does this directly:

    intermediario/cap20_ordenar_buscar.pylinhas 36 a 41
    ordenado = np.array([10, 20, 30, 40])
    print(np.searchsorted(ordenado, [5, 20, 25, 99]))
    
    idades = np.array([5, 17, 18, 40, 65, 90])
    faixas = np.digitize(idades, [18, 65])
    print(faixas)
    
    Output
    [0 1 2 4]
    [0 0 1 1 2 2]
    

    The ranges of the digitize are: 0 under 18, 1 from 18 to 64 and 2 from 65 onwards.

    Unique values and membership

    intermediario/cap20_ordenar_buscar.pylinhas 46 a 49
    a = np.array([3, 1, 2, 3, 1])
    print(np.unique(a), np.isin(a, [1, 2]))
    valores, primeira, contagem = np.unique(a, return_index=True, return_counts=True)
    print(valores, primeira, contagem)
    
    Output
    [1 2 3] [False  True  True False  True]
    [1 2 3] [1 2 0] [2 1 2]
    

    The np.isin asks, for each element, if it is in a set, and replaces the old np.in1d.

    The k largest, without sorting everything

    To find the 3 largest among millions of values, sorting everything is a waste. The np.argpartition puts the k desired elements at the front, without sorting them, in time proportional to the size of the array:

    intermediario/cap20_ordenar_buscar.pylinhas 54 a 56
    pontos = np.array([55, 91, 12, 78, 66, 99, 40])
    topo3 = np.argpartition(-pontos, 3)[:3]
    print(sorted(pontos[topo3].tolist(), reverse=True))
    
    Output
    [99, 91, 78]
    

    Stable sorts

    If two elements are equal, a stable sort maintains the original order between them. To ensure this in the argsort, pass kind="stable". This matters when you sort in stages (first by one key, then by another).

    Exercise 1

    Ranking of the best

    Write ranking(nomes, notas, k) that returns the names of the k best, from best to worst.