Pensamiento Computacional · Parcial modelo
Segundo parcial de Pensamiento Computacional, resuelto paso a paso
Es un parcial modelo, escrito por nosotros. No es el parcial de ninguna cátedra ni de ninguna fecha: está armado con el temario y el formato de los que se toman, para que puedas practicar con algo parecido.
Archivos y manejo de errores, NumPy, Pandas y Matplotlib: leer y escribir datos, que un dato sucio no tire abajo el programa, y sacar conclusiones de una tabla.
6 ejercicios, 33 preguntas, 120 minutos. Se aprueba con 60 sobre 100.
Las consignas
Seis ejercicios, dos horas, como el de verdad. El parcial real es en papel y a libro cerrado: no hay computadora para probar nada, así que acá también conviene leer el código con el dedo antes de contestar.
Todo es Python 3, y las bibliotecas van importadas con las convenciones de siempre: np para NumPy, pd para Pandas y plt para Matplotlib.
Cuando se pregunta qué imprime una línea, la respuesta es lo que sale
exactamente, con el formato de Python: un array se muestra
[2. 4.] y una división siempre devuelve float, así que 7 se
muestra 7.0. Las dos formas se aceptan igual, pero acostumbrate
a la que imprime la máquina.
Ejercicio 1 — Un array armado a mano 10 puntos
Leé este programa y contestá sin ejecutarlo. import numpy as np a = [3, 1, 4] b = [2, 5, 6] M = np.zeros((2, 3)) for j in range(3): M[0, j] = a[j] M[1, j] = b[j] - a[j] print(M.shape) print(M[:, 1]) print(M.sum(axis=1))
- a) ¿Cuánto vale M.shape? Escribilo con paréntesis.
- b) ¿Qué imprime print(M[:, 1])?
- c) ¿Qué imprime print(M.sum(axis=1))?
- d) ¿Cuánto vale M[1, 2]?
- e) np.zeros((2, 3)) lleva dos paréntesis. ¿Por qué?
Primero conviene escribir el array terminado, porque todo lo demás sale de mirarlo. El ciclo recorre las tres columnas y en cada una escribe las dos filas:
[ −1 4 2 ]]
La primera fila es a tal cual. La segunda es b − a posición a posición: 2−3 = −1, 5−1 = 4, 6−4 = 2.
a) np.zeros((2, 3)) crea 2 filas y 3 columnas, así que M.shape es (2, 3). Siempre en ese orden: shape[0] son las filas y shape[1] las columnas. Confundir uno con otro es lo que hace que un ciclo se vaya de rango sin que se entienda por qué.
b) [1. 4.]
Acá está la primera trampa del ejercicio, y es la que más se falla de NumPy. Los dos índices son [fila, columna], y los dos puntos significan "todas". Entonces:
M[1, :] → fila 1, todas las columnas → una FILA: [−1. 4. 2.]
Los dos puntos están del lado que no se fija. Si el que está fijo es el segundo índice, lo que sale es una columna, aunque se imprima acostada. Y los puntos en [1. 4.] no son decoración: np.zeros crea un array de float, así que todo lo que se guarde adentro se guarda como float.
c) [8. 5.]
Segunda trampa, y la regla para no volver a caer: el eje que ponés es el que desaparece. Con axis=1 desaparece la dimensión de las columnas, o sea que cada fila se colapsa a un número:
axis=0 → [3−1, 1+4, 4+2] = [2. 5. 6.] suma cada columna
Fijate que [2. 5. 6.] —el resultado de axis=0— es justo la lista b, y no por casualidad: la columna j vale a[j] y b[j]−a[j], que sumadas dan b[j]. Es un buen control de que el array está bien armado.
Sin axis, M.sum() suma todo y daría un solo número, 13.0.
d) Fila 1, columna 2: 2.0. Se cuenta desde cero en las dos direcciones.
e) La forma va como una tupla. La firma es np.zeros(shape, dtype): el primer argumento es la forma completa y el segundo es el tipo de dato. Escribir np.zeros(2, 3) le pasa un 3 donde esperaba un tipo, y tira TypeError. Por eso el doble paréntesis: el de afuera es el de la función y el de adentro es la tupla (2, 3).
Para una sola dimensión no hace falta: np.zeros(5) anda, porque un número solo ya alcanza para describir la forma.
Ejercicio 2 — try, except y finally 10 puntos
Este programa usa try, except y finally. Decí qué imprime y en qué orden. def leer_entero(texto): try: return int(texto) except ValueError: print("no es entero") return 0 finally: print("listo") print(leer_entero("7")) print(leer_entero("3.5"))
- a) ¿Qué error tira int("3.5")? Escribí el nombre.
- b) ¿Cuál es la PRIMERA línea que aparece en pantalla?
- c) ¿Cuántas líneas imprime el programa en total?
- d) Si el except dijera TypeError en vez de ValueError, ¿qué pasaría con leer_entero("3.5")?
- e) ¿Por qué conviene escribir except ValueError y no except a secas?
La salida completa, en orden:
7
no es entero
listo
0
a) ValueError. El tipo del argumento está bien —es un string, que es lo que int() espera— pero el valor no se puede leer como entero. Esa es exactamente la diferencia entre ValueError y TypeError: int("3.5") es ValueError, "a" + 1 es TypeError.
Ojo con este caso, que confunde: float("3.5") anda perfecto. Es int() el que no acepta un punto decimal adentro de la cadena.
b) listo, y acá está la trampa del ejercicio.
Lo intuitivo es pensar que el return int(texto) devuelve 7 y la función termina ahí, sin llegar nunca al finally. No es así: el finally corre siempre, incluso cuando hay un return en el medio. Python evalúa el return, se guarda el 7 de costado, ejecuta el finally, y recién entonces sale de la función. Así que el print("listo") de adentro pasa antes que el print(...) de afuera.
Y eso no es un capricho del lenguaje: es para qué existe el finally. Es el lugar donde se cierra un archivo o una conexión con la garantía de que se va a cerrar pase lo que pase — que salga bien, que falle, o que alguien haya puesto un return en el medio.
c) 5 líneas. Dos por cada llamada más una:
leer_entero("3.5") → "no es entero" (el except), "listo" (el finally), y el print de afuera muestra 0
d) El ValueError no queda atrapado. Un except sólo agarra el error que nombra. Con except TypeError, el ValueError pasa de largo, sube, y corta el programa. Pero el finally se ejecuta igual antes de que el error siga subiendo, así que "listo" aparece en pantalla y recién después el mensaje de error. El finally corre incluso cuando la excepción no se atrapó.
e) Porque el except pelado atrapa absolutamente todo, incluidos los errores que no tienen nada que ver. Si adentro del try escribiste mal el nombre de una variable, eso es un NameError; con except: queda atrapado, se ejecuta el bloque de error como si el dato hubiera estado mal, y el programa sigue andando con un resultado incorrecto y sin avisar. Un error que explota se arregla en cinco minutos; uno escondido puede quedar meses.
La regla práctica: un try chico, alrededor de la línea que puede fallar, y un except por cada error que de verdad esperás. La cátedra marca como mala práctica meter medio programa adentro de un try enorme, justamente porque después no se sabe qué parte falló.
Ejercicio 3 — Un .csv leído a mano 10 puntos
El archivo ventas.csv tiene una fila de títulos y tres de datos, y termina con un salto de línea después de la última fila. Este es el contenido completo (va sangrado para que se lea acá: las líneas del archivo no llevan espacios adelante): sucursal,unidades centro,120 norte,85 sur,140 Sobre ese archivo se corre este programa, sin Pandas: with open("ventas.csv") as f: texto = f.read() filas = texto.split("\n") print(len(filas)) print(filas[0].split(",")) campos = filas[1].split(",") print(campos[1])
- a) ¿Cuánto imprime len(filas)?
- b) El archivo tiene cuatro líneas de texto. ¿Por qué len(filas) no da 4?
- c) ¿Qué imprime print(filas[0].split(","))?
- d) Se imprime 120. ¿Qué hay guardado en campos[1]?
- e) Si se recorren las filas de datos y se suman las unidades, ¿cuánto da el total? (unidades)
a) 5, y es la trampa del ejercicio.
El archivo se ve como cuatro líneas, pero lo que hay guardado en el disco es una sola cadena:
Fijate el \n del final, después del 140. split("\n") corta en cada salto y devuelve lo que queda entre corte y corte; después del último salto no queda nada, y ese "nada" es una cadena vacía que igual ocupa un lugar en la lista:
b) El control para no depender de la intuición: una cadena partida por N separadores da siempre N+1 pedazos. Hay 4 saltos de línea, así que salen 5 pedazos. Si el archivo no terminara en salto, habría 3 saltos y darían 4.
Esto importa de verdad, no es una curiosidad: la cadena vacía no tiene comas, así que ''.split(",") devuelve [''] y pedirle el campo [1] tira IndexError. Es el error que aparece justo en la última vuelta del ciclo, cuando todo lo demás ya funcionó.
Las dos formas de esquivarlo:
if f != "": # saltear lo vacío
# o directamente
filas = texto.strip().split("\n")
Con f.readlines() el problema no aparece —devuelve una línea por fila, sin el pedazo vacío— pero cada línea viene con su \n pegado, y ahí hace falta .strip(). Un problema o el otro: no hay forma de leer un archivo sin pensar en los saltos de línea.
c) split devuelve una lista, y se imprime con corchetes y con las comillas de cada elemento: ['sucursal', 'unidades']. La coma que separaba se usó para cortar y ya no está adentro de ningún pedazo.
d) La cadena "120", no el número.
Se ve igual impreso, y esa es toda la dificultad. La prueba está en operar: campos[1] + campos[1] da "120120", que es lo que hace + entre cadenas. Para que sea un número hay que convertirlo: int(campos[1]).
Todo lo que sale de un archivo es texto, igual que todo lo que sale de input(). No hay excepción.
e) 120 + 85 + 140 = 345.
Y el programa que lo calcula tiene que hacer las tres cosas de este ejercicio a la vez: saltear la fila de títulos, saltear el pedazo vacío del final, y convertir con int() antes de sumar.
for fila in filas[1:]: # sin los títulos
if fila != "": # sin el vacío del final
campos = fila.split(",")
total = total + int(campos[1])
Control: el total tiene que ser mayor que el valor más grande de la columna (140) y menor que 3 × 140 = 420. 345 entra ahí ✓
Ejercicio 4 — El promedio con datos sucios 20 puntos
El archivo temperaturas.csv tiene una fila de títulos y cinco de datos. Dos vienen mal cargadas desde la estación: una dice s/d y en la otra el campo quedó vacío. ciudad,temp rosario,24 cordoba,s/d mendoza,19 salta, neuquen,17 Esta función tiene que devolver el promedio de las temperaturas que SÍ se pudieron leer, avisando por pantalla cuáles no. Está completa salvo por el espacio en blanco de la línea marcada. def promedio_valido(nombre): total = 0 validos = 0 with open(nombre) as f: lineas = f.readlines() for linea in lineas[1:]: campos = linea.__________.split(",") # <-- completar try: total = total + int(campos[1]) validos = validos + 1 except ValueError: print("dato invalido:", campos[0]) return total / validos print(promedio_valido("temperaturas.csv"))
- a) El for recorre lineas[1:] y no lineas. ¿Por qué?
- b) ¿Qué va en el espacio en blanco, para que el último campo no arrastre el salto de línea? Escribí sólo el nombre del método.
- c) ¿Cuántas veces se ejecuta el bloque del except?
- d) ¿Cuánto vale total cuando el for termina?
- e) ¿Qué número imprime el programa? (°C)
- f) El try envuelve sólo dos líneas y no el for entero. Si envolviera el for entero, ¿qué pasaría?
a) Para saltear la fila de títulos. lineas[0] es "ciudad,temp", y si entrara al ciclo, int("temp") tiraría ValueError y el archivo aparecería con un dato inválido de más.
Es una decisión que hay que tomar leyendo el enunciado, no de memoria: hay archivos con fila de títulos y archivos sin ella. Con títulos va lineas[1:]; sin títulos, lineas entero. Empezar en 1 en un archivo que no tiene encabezado se come el primer dato, y no avisa.
b) Va strip(), o sea campos = linea.strip().split(","). (rstrip() también sirve acá, porque el salto está sólo al final; strip() es la costumbre más segura porque limpia de los dos lados.) readlines() devuelve cada línea con su salto al final, así que la primera es 'rosario,24\n' y sin limpiarla el último campo quedaría '24\n'.
Un detalle fino: int('24\n') funciona igual, porque int() tolera espacios y saltos alrededor. Pero '24\n' == '24' da False, así que en cuanto el campo se compare con algo —un nombre de ciudad, una categoría— el programa deja de andar por un carácter que no se ve. Por eso el strip() va siempre, no sólo cuando hace falta.
c) 2 veces, y el programa imprime:
dato invalido: salta
Las dos por ValueError: int("s/d") falla porque hay letras, e int("") falla porque no hay nada. La cadena vacía es tan inválida como el texto, y es el caso que nadie prueba.
d) Se suman sólo las tres que se pudieron convertir:
e) 20.0 — y acá está la trampa fuerte del parcial.
60 / 5 = 12.0 (sobre el total de filas)
Las dos cuentas se ven igual de prolijas y las dos dan un número redondo. La diferencia es el denominador, y el enunciado lo dice sin subrayarlo: "el promedio de las temperaturas que sí se pudieron leer". Dividir por 5 es contar dos ciudades que nunca aportaron un grado, y da una temperatura que no existe en ningún lado — 12 °C, cuando el dato más bajo del archivo es 17.
Es el mismo error que aparece en cualquier enunciado con datos sucios, y se evita con una sola costumbre: si el ciclo tiene un contador de válidos, el promedio se divide por ese contador y no por el largo de la lista. Por eso la variable existe.
Control: un promedio siempre tiene que caer entre el mínimo y el máximo de los valores que promediaste. Los válidos van de 17 a 24, y 20 está adentro ✓. El 12 queda afuera, y eso solo ya delata la cuenta.
Le falta una cosa a esta función, dicho sea de paso: si el archivo tuviera todas las filas mal cargadas, validos quedaría en 0 y el return tiraría ZeroDivisionError. La versión prolija chequea if validos == 0 antes de dividir.
f) El primer dato malo cortaría el recorrido. Una excepción no vuelve al lugar donde ocurrió: sale hacia afuera hasta encontrar un except. Si el try abraza el for entero, saltar al except significa abandonar el ciclo, y el programa terminaría con total = 24 y validos = 1, o sea habiendo leído sólo Rosario.
Con el try adentro del ciclo, en cambio, cada vuelta es independiente: la que falla se avisa y se sigue con la siguiente. La regla es esa — el try va lo más chico posible, alrededor de la línea que puede fallar — y es exactamente la que la cátedra evalúa cuando pide "procesar el archivo aunque algunos valores estén mal cargados".
Ejercicio 5 — La tabla de aprobados 20 puntos
Con los datos de cuatro comisiones se arma un DataFrame, se hace una copia y sobre la copia se agrega una columna con el porcentaje de aprobados. import pandas as pd import matplotlib.pyplot as plt datos = {"comision": ["A", "B", "C", "D"], "inscriptos": [40, 25, 50, 35], "aprobados": [22, 20, 28, 14]} df = pd.DataFrame(datos) df2 = df.copy() df2["tasa"] = df2["aprobados"] * 100 / df2["inscriptos"] print(df.shape) print(df2[df2["tasa"] > 50]["comision"].tolist()) plt.bar(df2["comision"], df2["tasa"]) plt.title("Porcentaje de aprobados por comision") plt.show()
- a) ¿Qué imprime print(df.shape)? Escribilo con paréntesis.
- b) ¿Cuál es la tasa de aprobados de la comisión C? Poné sólo el número. (%)
- c) Después de la línea que agrega la columna tasa, ¿cuántas columnas tiene df?
- d) ¿Qué lista imprime df2[df2["tasa"] > 50]["comision"].tolist()? Escribí las letras separadas por comas.
- e) Mirando las cuatro comisiones, ¿cuál rindió mejor?
- f) El gráfico tiene título y se muestra. ¿Qué le falta para que se entienda sin leer el código?
Primero la columna que se agrega, que es de donde sale todo lo demás:
B 20 · 100 / 25 = 80 D 14 · 100 / 35 = 40
a) (4, 3): cuatro filas y tres columnas, en ese orden. El diccionario tenía tres claves, y cada clave es una columna.
b) 56%. La columna tasa se calcula de una sola vez para las cuatro filas: una columna de Pandas se comporta como un array de NumPy, así que la división y la multiplicación se aplican elemento por elemento sin ningún ciclo.
c) 3. Esta es la primera trampa del ejercicio.
df.copy() crea una tabla nueva, con sus propios datos. Agregarle una columna a df2 no toca df: el original sigue con comision, inscriptos y aprobados, y la copia queda con cuatro.
Lo que hay que ver es el contraste con la otra forma de escribirlo:
df2 = df → dos nombres para la MISMA tabla. Tocar df2 toca df
Sin el .copy(), después de esa línea df también tendría 4 columnas. Por eso las consignas dicen "un nuevo dataframe, copia del original": es una instrucción, no un adorno, y modificar el original cuando pedían una copia se descuenta.
d) ['A', 'B', 'C']. Se lee de adentro hacia afuera, en tres pasos:
df2[ esa máscara ] → las filas donde dio True
[ "comision" ].tolist() → de esas filas, sólo esa columna, como lista de Python
D queda afuera con 40%. Y ojo con qué devuelve cada paso: la comparación no devuelve las tasas grandes, devuelve una columna de True y False, una por fila. Es el mismo mecanismo que el indexado booleano de NumPy.
Para combinar dos condiciones se usa & y |, nunca and y or, y cada condición va entre paréntesis:
e) B, con el 80% — la segunda trampa, y la más importante del ejercicio.
La columna que salta a la vista es aprobados, y ahí C gana cómoda: 28 contra 20. Pero C tenía 50 inscriptos y B tenía 25. En proporción:
B es la única que pasa del 56%, y le saca 24 puntos a la que la sigue. Un número absoluto no se puede comparar entre grupos de distinto tamaño; para eso se calcula la tasa, que es exactamente lo que hace la columna nueva. La cuarta opción —"no se puede comparar"— da vuelta el argumento: que tengan distinta cantidad de inscriptos no impide comparar, es la razón por la que hay que porcentuar.
f) Faltan las etiquetas de los ejes. El título dice de qué es el gráfico, pero no qué mide cada eje ni en qué unidad: quien lo mire no sabe si las barras son cantidades de personas o porcentajes. Son dos líneas:
plt.ylabel("Aprobados (%)")
plt.ylim(0, 100)
Es el punto que más se descuenta cuando el parcial pide un gráfico, justamente porque es barato.
Las otras tres opciones, y por qué no: la leyenda no hace falta porque hay una sola serie —sirve cuando se dibujan dos o más y hay que distinguirlas—; el título solo no alcanza; y plt.plot() sería peor, no mejor: las comisiones son categorías sin orden, y una línea entre A, B, C y D sugeriría una evolución que no existe. Barras es lo que corresponde.
Y ya que están las barras: ese plt.ylim(0, 100) no es cosmético. En un gráfico de barras lo que se compara es la longitud, así que el eje tiene que arrancar en 0; si lo recortaras entre 40 y 80, la barra de B se vería varias veces más alta que la de D cuando en realidad es el doble. El eje elegido cambia la historia que cuenta el gráfico.
Ejercicio 6 — Encontrar y explicar los errores 30 puntos
Estas dos funciones tendrían que cargar un archivo de mediciones en un array de NumPy y devolver el promedio de una de sus columnas. El archivo med.csv NO tiene fila de títulos: cada línea es una temperatura y una humedad separadas por coma. 12,40 18,36 9,50 El programa tiene SEIS errores. Encontralos y, en cada caso, explicá por qué está mal. Mirá cada uno por separado, suponiendo los otros cinco ya corregidos: varios se tapan entre sí. import numpy as np def cargar(nombre): f = open(nombre, "w") lineas = f.readlines() f.close() datos = np.zeros(len(lineas), 2) i = 0 while i <= len(lineas): campos = lineas[i].strip().split(",") datos[i, 0] = float(campos[0]) datos[i, 1] = float(campos[1]) return datos def promedio_columna(datos, c): return datos[c, :].mean()
- a) El archivo se abre con open(nombre, "w"). ¿Qué está mal?
- b) ¿Cómo se escribe bien la línea del np.zeros?
- c) La condición del while dice i <= len(lineas). ¿Qué provoca y cómo se arregla?
- d) Adentro del while falta una línea, y sin ella el ciclo nunca termina. Escribila.
- e) El return datos está indentado adentro del while. ¿Qué consecuencia tiene?
- f) promedio_columna devuelve datos[c, :].mean(). ¿Cómo tendría que ser?
Los seis se miran de a uno, suponiendo los otros corregidos — si no, se tapan entre sí: el modo "w" corta el programa en la segunda línea, y el return de adentro del while hace que el ciclo no dé nunca una segunda vuelta.
Los seis, en el orden en que aparecen. Los cuatro primeros hacen ruido: el programa se corta y el mensaje de error dice dónde. Los dos últimos son peores, porque no rompen nada: devuelven un número, y el número está mal.
a) El modo. open(nombre, "w") abre para escribir. Hace dos cosas malas de una: readlines() sobre un archivo abierto en "w" no funciona, y —esto es lo grave— "w" vacía el archivo en el instante en que se abre, antes de que se escriba nada. O sea que el programa borra las mediciones y recién después falla. Va open(nombre, "r").
Mejor todavía, with open(nombre, "r") as f:, que cierra solo aunque salte un error en el medio. Con open y close sueltos, cualquiera de los otros cinco errores deja el archivo abierto.
b) La forma del array va como una tupla. np.zeros(shape, dtype): el segundo argumento es el tipo de dato, no la cantidad de columnas. np.zeros(len(lineas), 2) le pasa un 2 donde esperaba un tipo y tira TypeError. Se escribe con la forma adentro de un paréntesis más:
c) La condición del while. Con tres líneas, los índices válidos son 0, 1 y 2. Con i <= len(lineas) el ciclo también entra con i = 3 y pide lineas[3], que no existe: IndexError. Va i < len(lineas).
La regla que evita pensarlo cada vez: el último índice de una lista de n elementos es n−1, así que la condición de un recorrido completo es siempre con menor estricto.
d) Falta i = i + 1. Nada adentro del ciclo mueve el índice, así que la condición nunca deja de cumplirse y el programa se queda leyendo la misma línea para siempre. Es el error más típico del while y el único de los seis que no da ningún mensaje: la computadora simplemente se queda. Va al final del cuerpo del ciclo, después de cargar la fila.
(Si el i = i + 1 estuviera adentro de un if, pasaría lo mismo cada vez que la condición del if diera falso. Vale la pena mirar siempre a qué bloque pertenece.)
e) El return está adentro del while. Esta es la trampa del ejercicio, y la que más puntos cuesta en el parcial real: no tira ningún error. La función devuelve, prolijamente, un array con la forma correcta — sólo que después de UNA vuelta:
[ 0. 0. ]
[ 0. 0. ]]
Las filas que nunca se cargaron siguen con los ceros que puso np.zeros, y esos ceros después entran a los promedios como si fueran mediciones reales. El return tiene que estar afuera del while, alineado con él.
Por eso la cátedra pide marcar la indentación cuando el parcial se escribe a mano: un renglón corrido cuatro espacios cambia qué hace el programa, y no hay ningún error que lo delate.
f) Fila contra columna. Los dos índices de un array son [fila, columna], y los dos puntos significan "todas":
datos[:, c] → la COLUMNA c datos[:, 1] = [40. 36. 50.] → promedio 42.0
Otra vez un error que devuelve un número creíble: 27 es un promedio perfectamente razonable, sólo que mezcla una temperatura con una humedad en vez de promediar humedades. Un resultado que no explota no es un resultado correcto.
Las dos funciones corregidas:
def cargar(nombre):
with open(nombre, "r") as f:
lineas = f.readlines()
datos = np.zeros((len(lineas), 2))
i = 0
while i < len(lineas):
campos = lineas[i].strip().split(",")
datos[i, 0] = float(campos[0])
datos[i, 1] = float(campos[1])
i = i + 1
return datos
def promedio_columna(datos, c):
return datos[:, c].mean()
Con el archivo del enunciado: cargar("med.csv").shape da (3, 2), el promedio de la columna 0 es 13.0 —(12+18+9)/3— y el de la columna 1 es 42.0 —(40+36+50)/3.
Control, y es el que hay que hacer siempre con un archivo: la cantidad de filas del array tiene que coincidir con la cantidad de líneas del archivo, y cada promedio tiene que caer entre el mínimo y el máximo de su columna. Si aparece un cero en un promedio de temperaturas que van de 9 a 18, hay filas que quedaron sin cargar.
Y una cosa sobre cómo se corrige esto en el parcial: no alcanza con entregar el código arreglado. La consigna pide indicar y explicar cada error, y el corrector descuenta si falta la explicación aunque la versión corregida esté perfecta. Conviene numerarlos y escribir un renglón por error, como está acá arriba.
Leerlo resuelto no es lo mismo que rendirlo. Acá lo podés hacer entero sin que te corrija nada hasta entregarlo, igual que en el aula, y recién ahí te dice la nota, cómo se resolvía cada ejercicio y a qué temas te conviene volver.
Rendir el segundo parcial →